commit c798279f7da269ecd49630fa2e1fd6c1f2f8d036 Author: Abdusalom G'ayratov Date: Fri Nov 28 11:10:49 2025 +0500 INFRA: Set Up Project. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..f70d82d --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: android + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: ios + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..632f40b --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# customer + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..43024b7 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.emart.customer" + compileSdk = 36 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.emart.customer" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 26 + targetSdk = 36 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + isMinifyEnabled = false + isShrinkResources = false + } + } +} + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") + implementation("com.android.billingclient:billing:7.1.1") + implementation("com.google.firebase:firebase-auth:21.1.0") + implementation("androidx.core:core:1.13.1") + implementation("com.razorpay:checkout:1.6.33") + implementation("com.google.android.material:material:1.12.0") +} + +flutter { + source = "../.." +} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..4d10887 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,47 @@ +{ + "project_info": { + "project_number": "893074789710", + "firebase_url": "https://fondexuzb-default-rtdb.firebaseio.com", + "project_id": "fondexuzb", + "storage_bucket": "fondexuzb.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:893074789710:android:05002c15a64cf1e0c4ba1f", + "android_client_info": { + "package_name": "com.emart.customer" + } + }, + "oauth_client": [ + { + "client_id": "893074789710-nqkeis20f6b64luas7g44c836fvi0qms.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyALZhdy7Rw3jffipxsDvvz7_C_b4teVg1k" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "893074789710-nqkeis20f6b64luas7g44c836fvi0qms.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "893074789710-pv12m4nhe82a4ueg9sb2pgt42r0e5da3.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.emart.customer" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..305ac58 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/emart/customer/MainActivity.kt b/android/app/src/main/kotlin/com/emart/customer/MainActivity.kt new file mode 100644 index 0000000..21c78d3 --- /dev/null +++ b/android/app/src/main/kotlin/com/emart/customer/MainActivity.kt @@ -0,0 +1,6 @@ +package com.emart.customer + +import io.flutter.embedding.android.FlutterFragmentActivity + +class MainActivity : FlutterFragmentActivity() { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..345888d --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c18ca62 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..328a8d4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..0edd7e3 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..0edd7e3 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..b4f1709 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..3d14d2d Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..13a4c4f Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..13a4c4f Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3cb9936 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..e7f70bb Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ac80499 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..ac80499 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..0ad321f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..e1a9322 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..0f79dc8 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..0f79dc8 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..89b8d05 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..2ffee54 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d3a6e5b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..d3a6e5b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/play_store_512.png b/android/app/src/main/res/play_store_512.png new file mode 100644 index 0000000..f0c67d2 Binary files /dev/null and b/android/app/src/main/res/play_store_512.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d2aa524 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/provider_paths.xml b/android/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..ffa74ab --- /dev/null +++ b/android/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..36fd3a3 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/assets/fonts/essential_sans_bold.otf b/assets/fonts/essential_sans_bold.otf new file mode 100644 index 0000000..4492386 Binary files /dev/null and b/assets/fonts/essential_sans_bold.otf differ diff --git a/assets/fonts/essential_sans_medium.otf b/assets/fonts/essential_sans_medium.otf new file mode 100644 index 0000000..6c15de2 Binary files /dev/null and b/assets/fonts/essential_sans_medium.otf differ diff --git a/assets/fonts/essential_sans_regular.otf b/assets/fonts/essential_sans_regular.otf new file mode 100644 index 0000000..0089f54 Binary files /dev/null and b/assets/fonts/essential_sans_regular.otf differ diff --git a/assets/fonts/essential_sans_semi_bold.otf b/assets/fonts/essential_sans_semi_bold.otf new file mode 100644 index 0000000..ea60ef4 Binary files /dev/null and b/assets/fonts/essential_sans_semi_bold.otf differ diff --git a/assets/icons/app_logo.png b/assets/icons/app_logo.png new file mode 100644 index 0000000..f5bb12f Binary files /dev/null and b/assets/icons/app_logo.png differ diff --git a/assets/icons/delete_dialog.gif b/assets/icons/delete_dialog.gif new file mode 100644 index 0000000..dd81256 Binary files /dev/null and b/assets/icons/delete_dialog.gif differ diff --git a/assets/icons/dropoff.png b/assets/icons/dropoff.png new file mode 100644 index 0000000..0eefb0a Binary files /dev/null and b/assets/icons/dropoff.png differ diff --git a/assets/icons/ic_advertisement.svg b/assets/icons/ic_advertisement.svg new file mode 100644 index 0000000..4f2e067 --- /dev/null +++ b/assets/icons/ic_advertisement.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_alarm_clock.svg b/assets/icons/ic_alarm_clock.svg new file mode 100644 index 0000000..4fbf714 --- /dev/null +++ b/assets/icons/ic_alarm_clock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_apple.svg b/assets/icons/ic_apple.svg new file mode 100644 index 0000000..4cf446c --- /dev/null +++ b/assets/icons/ic_apple.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/ic_arrow_left.png b/assets/icons/ic_arrow_left.png new file mode 100644 index 0000000..76918d6 Binary files /dev/null and b/assets/icons/ic_arrow_left.png differ diff --git a/assets/icons/ic_arrows_clockwise.png b/assets/icons/ic_arrows_clockwise.png new file mode 100644 index 0000000..3fe2fd2 Binary files /dev/null and b/assets/icons/ic_arrows_clockwise.png differ diff --git a/assets/icons/ic_book.svg b/assets/icons/ic_book.svg new file mode 100644 index 0000000..530dfea --- /dev/null +++ b/assets/icons/ic_book.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_booking_cab.svg b/assets/icons/ic_booking_cab.svg new file mode 100644 index 0000000..5a4234b --- /dev/null +++ b/assets/icons/ic_booking_cab.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_building.svg b/assets/icons/ic_building.svg new file mode 100644 index 0000000..6d7186b --- /dev/null +++ b/assets/icons/ic_building.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_cab_current_location.png b/assets/icons/ic_cab_current_location.png new file mode 100644 index 0000000..b4eded3 Binary files /dev/null and b/assets/icons/ic_cab_current_location.png differ diff --git a/assets/icons/ic_cab_destination.png b/assets/icons/ic_cab_destination.png new file mode 100644 index 0000000..f994186 Binary files /dev/null and b/assets/icons/ic_cab_destination.png differ diff --git a/assets/icons/ic_cab_pickup.png b/assets/icons/ic_cab_pickup.png new file mode 100644 index 0000000..ed6aa7a Binary files /dev/null and b/assets/icons/ic_cab_pickup.png differ diff --git a/assets/icons/ic_cab_set_location.svg b/assets/icons/ic_cab_set_location.svg new file mode 100644 index 0000000..5b9d24d --- /dev/null +++ b/assets/icons/ic_cab_set_location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_cashback_Offer.svg b/assets/icons/ic_cashback_Offer.svg new file mode 100644 index 0000000..76e393c --- /dev/null +++ b/assets/icons/ic_cashback_Offer.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ic_change_language.svg b/assets/icons/ic_change_language.svg new file mode 100644 index 0000000..118dc29 --- /dev/null +++ b/assets/icons/ic_change_language.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/ic_check_small.svg b/assets/icons/ic_check_small.svg new file mode 100644 index 0000000..2be0088 --- /dev/null +++ b/assets/icons/ic_check_small.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_close.svg b/assets/icons/ic_close.svg new file mode 100644 index 0000000..1bbdf4a --- /dev/null +++ b/assets/icons/ic_close.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_copy.svg b/assets/icons/ic_copy.svg new file mode 100644 index 0000000..d4291b0 --- /dev/null +++ b/assets/icons/ic_copy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_coupon.svg b/assets/icons/ic_coupon.svg new file mode 100644 index 0000000..facf66d --- /dev/null +++ b/assets/icons/ic_coupon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/ic_coupon_parcel.svg b/assets/icons/ic_coupon_parcel.svg new file mode 100644 index 0000000..e81fb5e --- /dev/null +++ b/assets/icons/ic_coupon_parcel.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_credit.svg b/assets/icons/ic_credit.svg new file mode 100644 index 0000000..f7b1899 --- /dev/null +++ b/assets/icons/ic_credit.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_debit.svg b/assets/icons/ic_debit.svg new file mode 100644 index 0000000..3490b1c --- /dev/null +++ b/assets/icons/ic_debit.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_delete.svg b/assets/icons/ic_delete.svg new file mode 100644 index 0000000..4c3b1f2 --- /dev/null +++ b/assets/icons/ic_delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_delete_address.svg b/assets/icons/ic_delete_address.svg new file mode 100644 index 0000000..6a5f56b --- /dev/null +++ b/assets/icons/ic_delete_address.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/ic_dinin_order.svg b/assets/icons/ic_dinin_order.svg new file mode 100644 index 0000000..30ba305 --- /dev/null +++ b/assets/icons/ic_dinin_order.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_distance_parcel.svg b/assets/icons/ic_distance_parcel.svg new file mode 100644 index 0000000..96d056f --- /dev/null +++ b/assets/icons/ic_distance_parcel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_down.svg b/assets/icons/ic_down.svg new file mode 100644 index 0000000..98a4846 --- /dev/null +++ b/assets/icons/ic_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_duration.svg b/assets/icons/ic_duration.svg new file mode 100644 index 0000000..3c420fe --- /dev/null +++ b/assets/icons/ic_duration.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/ic_edit.svg b/assets/icons/ic_edit.svg new file mode 100644 index 0000000..9534501 --- /dev/null +++ b/assets/icons/ic_edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_edit_address.svg b/assets/icons/ic_edit_address.svg new file mode 100644 index 0000000..d434dad --- /dev/null +++ b/assets/icons/ic_edit_address.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_fav.svg b/assets/icons/ic_fav.svg new file mode 100644 index 0000000..e21e555 --- /dev/null +++ b/assets/icons/ic_fav.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_focus.svg b/assets/icons/ic_focus.svg new file mode 100644 index 0000000..f78a005 --- /dev/null +++ b/assets/icons/ic_focus.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_folder.svg b/assets/icons/ic_folder.svg new file mode 100644 index 0000000..f2029b6 --- /dev/null +++ b/assets/icons/ic_folder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_free_delivery.svg b/assets/icons/ic_free_delivery.svg new file mode 100644 index 0000000..420d8f9 --- /dev/null +++ b/assets/icons/ic_free_delivery.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_gift_code.svg b/assets/icons/ic_gift_code.svg new file mode 100644 index 0000000..629cb37 --- /dev/null +++ b/assets/icons/ic_gift_code.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_gift_pin.svg b/assets/icons/ic_gift_pin.svg new file mode 100644 index 0000000..c04b8a4 --- /dev/null +++ b/assets/icons/ic_gift_pin.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_google.svg b/assets/icons/ic_google.svg new file mode 100644 index 0000000..7ff275b --- /dev/null +++ b/assets/icons/ic_google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_history.svg b/assets/icons/ic_history.svg new file mode 100644 index 0000000..0d67908 --- /dev/null +++ b/assets/icons/ic_history.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_home.svg b/assets/icons/ic_home.svg new file mode 100644 index 0000000..1f6cfed --- /dev/null +++ b/assets/icons/ic_home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_home_add.svg b/assets/icons/ic_home_add.svg new file mode 100644 index 0000000..ab2e476 --- /dev/null +++ b/assets/icons/ic_home_add.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_home_cab.svg b/assets/icons/ic_home_cab.svg new file mode 100644 index 0000000..2bcbacd --- /dev/null +++ b/assets/icons/ic_home_cab.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_home_parcel.svg b/assets/icons/ic_home_parcel.svg new file mode 100644 index 0000000..08b68e6 --- /dev/null +++ b/assets/icons/ic_home_parcel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_india.png b/assets/icons/ic_india.png new file mode 100644 index 0000000..f04d24f Binary files /dev/null and b/assets/icons/ic_india.png differ diff --git a/assets/icons/ic_intercity.svg b/assets/icons/ic_intercity.svg new file mode 100644 index 0000000..f5f3f40 --- /dev/null +++ b/assets/icons/ic_intercity.svg @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ic_light_dark.svg b/assets/icons/ic_light_dark.svg new file mode 100644 index 0000000..7f35a2e --- /dev/null +++ b/assets/icons/ic_light_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_like.svg b/assets/icons/ic_like.svg new file mode 100644 index 0000000..4908423 --- /dev/null +++ b/assets/icons/ic_like.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_like_fill.svg b/assets/icons/ic_like_fill.svg new file mode 100644 index 0000000..ec019e6 --- /dev/null +++ b/assets/icons/ic_like_fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_location.png b/assets/icons/ic_location.png new file mode 100644 index 0000000..cf4998b Binary files /dev/null and b/assets/icons/ic_location.png differ diff --git a/assets/icons/ic_location.svg b/assets/icons/ic_location.svg new file mode 100644 index 0000000..cddd959 --- /dev/null +++ b/assets/icons/ic_location.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_location_pin.svg b/assets/icons/ic_location_pin.svg new file mode 100644 index 0000000..55e7c55 --- /dev/null +++ b/assets/icons/ic_location_pin.svg @@ -0,0 +1,8 @@ + + + + diff --git a/assets/icons/ic_lock.svg b/assets/icons/ic_lock.svg new file mode 100644 index 0000000..e037270 --- /dev/null +++ b/assets/icons/ic_lock.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_logout.svg b/assets/icons/ic_logout.svg new file mode 100644 index 0000000..cec1161 --- /dev/null +++ b/assets/icons/ic_logout.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_mail.svg b/assets/icons/ic_mail.svg new file mode 100644 index 0000000..ea5d11e --- /dev/null +++ b/assets/icons/ic_mail.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_map_distance.svg b/assets/icons/ic_map_distance.svg new file mode 100644 index 0000000..a87e0e9 --- /dev/null +++ b/assets/icons/ic_map_distance.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_map_draw.svg b/assets/icons/ic_map_draw.svg new file mode 100644 index 0000000..8a0e802 --- /dev/null +++ b/assets/icons/ic_map_draw.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/assets/icons/ic_message.png b/assets/icons/ic_message.png new file mode 100644 index 0000000..7e7d6ba Binary files /dev/null and b/assets/icons/ic_message.png differ diff --git a/assets/icons/ic_mini_car.png b/assets/icons/ic_mini_car.png new file mode 100644 index 0000000..1f792fa Binary files /dev/null and b/assets/icons/ic_mini_car.png differ diff --git a/assets/icons/ic_mobile.svg b/assets/icons/ic_mobile.svg new file mode 100644 index 0000000..a190eed --- /dev/null +++ b/assets/icons/ic_mobile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_more_one.svg b/assets/icons/ic_more_one.svg new file mode 100644 index 0000000..6760424 --- /dev/null +++ b/assets/icons/ic_more_one.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/ic_mybooking_parcel.svg b/assets/icons/ic_mybooking_parcel.svg new file mode 100644 index 0000000..5a4234b --- /dev/null +++ b/assets/icons/ic_mybooking_parcel.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_nonveg.svg b/assets/icons/ic_nonveg.svg new file mode 100644 index 0000000..7d3831c --- /dev/null +++ b/assets/icons/ic_nonveg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_one_way.svg b/assets/icons/ic_one_way.svg new file mode 100644 index 0000000..7e03624 --- /dev/null +++ b/assets/icons/ic_one_way.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_orders.svg b/assets/icons/ic_orders.svg new file mode 100644 index 0000000..98de511 --- /dev/null +++ b/assets/icons/ic_orders.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_password_close.svg b/assets/icons/ic_password_close.svg new file mode 100644 index 0000000..5e48298 --- /dev/null +++ b/assets/icons/ic_password_close.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_password_show.svg b/assets/icons/ic_password_show.svg new file mode 100644 index 0000000..9e95fa4 --- /dev/null +++ b/assets/icons/ic_password_show.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_phone.svg b/assets/icons/ic_phone.svg new file mode 100644 index 0000000..a3f45c3 --- /dev/null +++ b/assets/icons/ic_phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_phone_call.svg b/assets/icons/ic_phone_call.svg new file mode 100644 index 0000000..fcf8cd2 --- /dev/null +++ b/assets/icons/ic_phone_call.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/ic_pickup.png b/assets/icons/ic_pickup.png new file mode 100644 index 0000000..6a62053 Binary files /dev/null and b/assets/icons/ic_pickup.png differ diff --git a/assets/icons/ic_picture_one.svg b/assets/icons/ic_picture_one.svg new file mode 100644 index 0000000..99cbaaa --- /dev/null +++ b/assets/icons/ic_picture_one.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_plus.svg b/assets/icons/ic_plus.svg new file mode 100644 index 0000000..315db5e --- /dev/null +++ b/assets/icons/ic_plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_privacy_policy.svg b/assets/icons/ic_privacy_policy.svg new file mode 100644 index 0000000..7d7bbb2 --- /dev/null +++ b/assets/icons/ic_privacy_policy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_profile.svg b/assets/icons/ic_profile.svg new file mode 100644 index 0000000..10794d8 --- /dev/null +++ b/assets/icons/ic_profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ic_profile_parcel.svg b/assets/icons/ic_profile_parcel.svg new file mode 100644 index 0000000..c221037 --- /dev/null +++ b/assets/icons/ic_profile_parcel.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_rate.svg b/assets/icons/ic_rate.svg new file mode 100644 index 0000000..84902e3 --- /dev/null +++ b/assets/icons/ic_rate.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_rate_parcel.svg b/assets/icons/ic_rate_parcel.svg new file mode 100644 index 0000000..9ad3fbb --- /dev/null +++ b/assets/icons/ic_rate_parcel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_redeem.svg b/assets/icons/ic_redeem.svg new file mode 100644 index 0000000..cb71172 --- /dev/null +++ b/assets/icons/ic_redeem.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/ic_refer.svg b/assets/icons/ic_refer.svg new file mode 100644 index 0000000..7394de2 --- /dev/null +++ b/assets/icons/ic_refer.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/ic_restaurant_chat.svg b/assets/icons/ic_restaurant_chat.svg new file mode 100644 index 0000000..15345c0 --- /dev/null +++ b/assets/icons/ic_restaurant_chat.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/ic_restaurant_driver.svg b/assets/icons/ic_restaurant_driver.svg new file mode 100644 index 0000000..9a5814a --- /dev/null +++ b/assets/icons/ic_restaurant_driver.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/ic_ride.svg b/assets/icons/ic_ride.svg new file mode 100644 index 0000000..3e1f5c4 --- /dev/null +++ b/assets/icons/ic_ride.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ic_round_trip.svg b/assets/icons/ic_round_trip.svg new file mode 100644 index 0000000..480a725 --- /dev/null +++ b/assets/icons/ic_round_trip.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/ic_scan_code.svg b/assets/icons/ic_scan_code.svg new file mode 100644 index 0000000..8ee62e7 --- /dev/null +++ b/assets/icons/ic_scan_code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_search.svg b/assets/icons/ic_search.svg new file mode 100644 index 0000000..23e54cb --- /dev/null +++ b/assets/icons/ic_search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_send.svg b/assets/icons/ic_send.svg new file mode 100644 index 0000000..2001430 --- /dev/null +++ b/assets/icons/ic_send.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_send_one.svg b/assets/icons/ic_send_one.svg new file mode 100644 index 0000000..ed718c3 --- /dev/null +++ b/assets/icons/ic_send_one.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_share.svg b/assets/icons/ic_share.svg new file mode 100644 index 0000000..cd5d611 --- /dev/null +++ b/assets/icons/ic_share.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_shoping_cart.svg b/assets/icons/ic_shoping_cart.svg new file mode 100644 index 0000000..af967d7 --- /dev/null +++ b/assets/icons/ic_shoping_cart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_star.svg b/assets/icons/ic_star.svg new file mode 100644 index 0000000..f440104 --- /dev/null +++ b/assets/icons/ic_star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_start.svg b/assets/icons/ic_start.svg new file mode 100644 index 0000000..bdba11b --- /dev/null +++ b/assets/icons/ic_start.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_taxi.png b/assets/icons/ic_taxi.png new file mode 100644 index 0000000..294b00c Binary files /dev/null and b/assets/icons/ic_taxi.png differ diff --git a/assets/icons/ic_tearm_condition.svg b/assets/icons/ic_tearm_condition.svg new file mode 100644 index 0000000..5b30a08 --- /dev/null +++ b/assets/icons/ic_tearm_condition.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_time.svg b/assets/icons/ic_time.svg new file mode 100644 index 0000000..606bce0 --- /dev/null +++ b/assets/icons/ic_time.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_timer.svg b/assets/icons/ic_timer.svg new file mode 100644 index 0000000..d671adc --- /dev/null +++ b/assets/icons/ic_timer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_upload_parcel.svg b/assets/icons/ic_upload_parcel.svg new file mode 100644 index 0000000..90d6c2a --- /dev/null +++ b/assets/icons/ic_upload_parcel.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/ic_user.svg b/assets/icons/ic_user.svg new file mode 100644 index 0000000..f223c38 --- /dev/null +++ b/assets/icons/ic_user.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_veg.svg b/assets/icons/ic_veg.svg new file mode 100644 index 0000000..894fde4 --- /dev/null +++ b/assets/icons/ic_veg.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/ic_view_grid_list.svg b/assets/icons/ic_view_grid_list.svg new file mode 100644 index 0000000..e32cc31 --- /dev/null +++ b/assets/icons/ic_view_grid_list.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/assets/icons/ic_wallet.svg b/assets/icons/ic_wallet.svg new file mode 100644 index 0000000..513ad88 --- /dev/null +++ b/assets/icons/ic_wallet.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_wallet_cab.svg b/assets/icons/ic_wallet_cab.svg new file mode 100644 index 0000000..adedc6a --- /dev/null +++ b/assets/icons/ic_wallet_cab.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_wallet_parcel.svg b/assets/icons/ic_wallet_parcel.svg new file mode 100644 index 0000000..168d378 --- /dev/null +++ b/assets/icons/ic_wallet_parcel.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_wechat.svg b/assets/icons/ic_wechat.svg new file mode 100644 index 0000000..4982ab1 --- /dev/null +++ b/assets/icons/ic_wechat.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ic_weight_parcel.svg b/assets/icons/ic_weight_parcel.svg new file mode 100644 index 0000000..05f6cde --- /dev/null +++ b/assets/icons/ic_weight_parcel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ic_work.svg b/assets/icons/ic_work.svg new file mode 100644 index 0000000..e6a5fdc --- /dev/null +++ b/assets/icons/ic_work.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/offer_icon.png b/assets/icons/offer_icon.png new file mode 100644 index 0000000..8a4500d Binary files /dev/null and b/assets/icons/offer_icon.png differ diff --git a/assets/icons/pickup.png b/assets/icons/pickup.png new file mode 100644 index 0000000..ed60549 Binary files /dev/null and b/assets/icons/pickup.png differ diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 0000000..725ad48 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/dine_in_bg.png b/assets/images/dine_in_bg.png new file mode 100644 index 0000000..22ac1f5 Binary files /dev/null and b/assets/images/dine_in_bg.png differ diff --git a/assets/images/dropoff.png b/assets/images/dropoff.png new file mode 100644 index 0000000..9cc6128 Binary files /dev/null and b/assets/images/dropoff.png differ diff --git a/assets/images/flutterwave_logo.png b/assets/images/flutterwave_logo.png new file mode 100644 index 0000000..58c80a1 Binary files /dev/null and b/assets/images/flutterwave_logo.png differ diff --git a/assets/images/food_delivery.gif b/assets/images/food_delivery.gif new file mode 100644 index 0000000..6689767 Binary files /dev/null and b/assets/images/food_delivery.gif differ diff --git a/assets/images/food_delivery.png b/assets/images/food_delivery.png new file mode 100644 index 0000000..b31de23 Binary files /dev/null and b/assets/images/food_delivery.png differ diff --git a/assets/images/ic_avavilable.png b/assets/images/ic_avavilable.png new file mode 100644 index 0000000..6353706 Binary files /dev/null and b/assets/images/ic_avavilable.png differ diff --git a/assets/images/ic_cab.png b/assets/images/ic_cab.png new file mode 100644 index 0000000..3aa5b94 Binary files /dev/null and b/assets/images/ic_cab.png differ diff --git a/assets/images/ic_cash.png b/assets/images/ic_cash.png new file mode 100644 index 0000000..58e5dcd Binary files /dev/null and b/assets/images/ic_cash.png differ diff --git a/assets/images/ic_coupon_image.png b/assets/images/ic_coupon_image.png new file mode 100644 index 0000000..ea27c2f Binary files /dev/null and b/assets/images/ic_coupon_image.png differ diff --git a/assets/images/ic_dinin.svg b/assets/images/ic_dinin.svg new file mode 100644 index 0000000..bc585ca --- /dev/null +++ b/assets/images/ic_dinin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/ic_favourite.svg b/assets/images/ic_favourite.svg new file mode 100644 index 0000000..b08dd25 --- /dev/null +++ b/assets/images/ic_favourite.svg @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ic_favourite_profile.svg b/assets/images/ic_favourite_profile.svg new file mode 100644 index 0000000..f3e6bbe --- /dev/null +++ b/assets/images/ic_favourite_profile.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/ic_gift.svg b/assets/images/ic_gift.svg new file mode 100644 index 0000000..2ef458c --- /dev/null +++ b/assets/images/ic_gift.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/ic_logo.png b/assets/images/ic_logo.png new file mode 100644 index 0000000..49a75e7 Binary files /dev/null and b/assets/images/ic_logo.png differ diff --git a/assets/images/ic_logout.gif b/assets/images/ic_logout.gif new file mode 100644 index 0000000..7a86fbc Binary files /dev/null and b/assets/images/ic_logout.gif differ diff --git a/assets/images/ic_new_arrival_bg.png b/assets/images/ic_new_arrival_bg.png new file mode 100644 index 0000000..b8f8654 Binary files /dev/null and b/assets/images/ic_new_arrival_bg.png differ diff --git a/assets/images/ic_new_arrival_dinein.png b/assets/images/ic_new_arrival_dinein.png new file mode 100644 index 0000000..5ed210c Binary files /dev/null and b/assets/images/ic_new_arrival_dinein.png differ diff --git a/assets/images/ic_product_bg_1.png b/assets/images/ic_product_bg_1.png new file mode 100644 index 0000000..ca00f44 Binary files /dev/null and b/assets/images/ic_product_bg_1.png differ diff --git a/assets/images/ic_product_bg_2.png b/assets/images/ic_product_bg_2.png new file mode 100644 index 0000000..00ca3a8 Binary files /dev/null and b/assets/images/ic_product_bg_2.png differ diff --git a/assets/images/ic_product_bg_3.png b/assets/images/ic_product_bg_3.png new file mode 100644 index 0000000..d8cc1fd Binary files /dev/null and b/assets/images/ic_product_bg_3.png differ diff --git a/assets/images/ic_profile.svg b/assets/images/ic_profile.svg new file mode 100644 index 0000000..3bde109 --- /dev/null +++ b/assets/images/ic_profile.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/ic_restaurant.svg b/assets/images/ic_restaurant.svg new file mode 100644 index 0000000..6b98a3d --- /dev/null +++ b/assets/images/ic_restaurant.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/ic_table.gif b/assets/images/ic_table.gif new file mode 100644 index 0000000..f06ed74 Binary files /dev/null and b/assets/images/ic_table.gif differ diff --git a/assets/images/ic_timer.gif b/assets/images/ic_timer.gif new file mode 100644 index 0000000..ac7c231 Binary files /dev/null and b/assets/images/ic_timer.gif differ diff --git a/assets/images/ic_tips.svg b/assets/images/ic_tips.svg new file mode 100644 index 0000000..4c52e3a --- /dev/null +++ b/assets/images/ic_tips.svg @@ -0,0 +1,424 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ic_wallet.png b/assets/images/ic_wallet.png new file mode 100644 index 0000000..8c70ed9 Binary files /dev/null and b/assets/images/ic_wallet.png differ diff --git a/assets/images/image_1.png b/assets/images/image_1.png new file mode 100644 index 0000000..2ce5abe Binary files /dev/null and b/assets/images/image_1.png differ diff --git a/assets/images/image_2.png b/assets/images/image_2.png new file mode 100644 index 0000000..e036957 Binary files /dev/null and b/assets/images/image_2.png differ diff --git a/assets/images/image_3.png b/assets/images/image_3.png new file mode 100644 index 0000000..3851a4c Binary files /dev/null and b/assets/images/image_3.png differ diff --git a/assets/images/image_parcel.png b/assets/images/image_parcel.png new file mode 100644 index 0000000..5dfa5e4 Binary files /dev/null and b/assets/images/image_parcel.png differ diff --git a/assets/images/image_parcel_scheduled.png b/assets/images/image_parcel_scheduled.png new file mode 100644 index 0000000..6fe982c Binary files /dev/null and b/assets/images/image_parcel_scheduled.png differ diff --git a/assets/images/image_status_timeline.png b/assets/images/image_status_timeline.png new file mode 100644 index 0000000..c0794b6 Binary files /dev/null and b/assets/images/image_status_timeline.png differ diff --git a/assets/images/image_timeline.png b/assets/images/image_timeline.png new file mode 100644 index 0000000..127011f Binary files /dev/null and b/assets/images/image_timeline.png differ diff --git a/assets/images/img_onboarding.png b/assets/images/img_onboarding.png new file mode 100644 index 0000000..a6bd933 Binary files /dev/null and b/assets/images/img_onboarding.png differ diff --git a/assets/images/img_ride_driver.png b/assets/images/img_ride_driver.png new file mode 100644 index 0000000..595513a Binary files /dev/null and b/assets/images/img_ride_driver.png differ diff --git a/assets/images/location.gif b/assets/images/location.gif new file mode 100644 index 0000000..5422cb4 Binary files /dev/null and b/assets/images/location.gif differ diff --git a/assets/images/location_bg.png b/assets/images/location_bg.png new file mode 100644 index 0000000..672e7b5 Binary files /dev/null and b/assets/images/location_bg.png differ diff --git a/assets/images/location_black3x.png b/assets/images/location_black3x.png new file mode 100644 index 0000000..ed5ec88 Binary files /dev/null and b/assets/images/location_black3x.png differ diff --git a/assets/images/location_orange3x.png b/assets/images/location_orange3x.png new file mode 100644 index 0000000..8793da0 Binary files /dev/null and b/assets/images/location_orange3x.png differ diff --git a/assets/images/login.gif b/assets/images/login.gif new file mode 100644 index 0000000..bcedb2c Binary files /dev/null and b/assets/images/login.gif differ diff --git a/assets/images/maintenance.png b/assets/images/maintenance.png new file mode 100644 index 0000000..a29d5a1 Binary files /dev/null and b/assets/images/maintenance.png differ diff --git a/assets/images/map_selected.png b/assets/images/map_selected.png new file mode 100644 index 0000000..e7f2f0e Binary files /dev/null and b/assets/images/map_selected.png differ diff --git a/assets/images/map_unselected.png b/assets/images/map_unselected.png new file mode 100644 index 0000000..d4f5a7b Binary files /dev/null and b/assets/images/map_unselected.png differ diff --git a/assets/images/mercado-pago.png b/assets/images/mercado-pago.png new file mode 100644 index 0000000..f04df52 Binary files /dev/null and b/assets/images/mercado-pago.png differ diff --git a/assets/images/midtrans.png b/assets/images/midtrans.png new file mode 100644 index 0000000..72011b8 Binary files /dev/null and b/assets/images/midtrans.png differ diff --git a/assets/images/offer_gif.gif b/assets/images/offer_gif.gif new file mode 100644 index 0000000..601712a Binary files /dev/null and b/assets/images/offer_gif.gif differ diff --git a/assets/images/offer_icon.png b/assets/images/offer_icon.png new file mode 100644 index 0000000..8a4500d Binary files /dev/null and b/assets/images/offer_icon.png differ diff --git a/assets/images/onboarding_bg.png b/assets/images/onboarding_bg.png new file mode 100644 index 0000000..f43eaf0 Binary files /dev/null and b/assets/images/onboarding_bg.png differ diff --git a/assets/images/orange_money.png b/assets/images/orange_money.png new file mode 100644 index 0000000..46cebee Binary files /dev/null and b/assets/images/orange_money.png differ diff --git a/assets/images/parcel_order_successfully_placed.png b/assets/images/parcel_order_successfully_placed.png new file mode 100644 index 0000000..32a206d Binary files /dev/null and b/assets/images/parcel_order_successfully_placed.png differ diff --git a/assets/images/payfast.png b/assets/images/payfast.png new file mode 100644 index 0000000..4bc976c Binary files /dev/null and b/assets/images/payfast.png differ diff --git a/assets/images/paypal.png b/assets/images/paypal.png new file mode 100644 index 0000000..e8651db Binary files /dev/null and b/assets/images/paypal.png differ diff --git a/assets/images/paystack.png b/assets/images/paystack.png new file mode 100644 index 0000000..a623879 Binary files /dev/null and b/assets/images/paystack.png differ diff --git a/assets/images/paytm.png b/assets/images/paytm.png new file mode 100644 index 0000000..27cf7e2 Binary files /dev/null and b/assets/images/paytm.png differ diff --git a/assets/images/pickup.png b/assets/images/pickup.png new file mode 100644 index 0000000..6a62053 Binary files /dev/null and b/assets/images/pickup.png differ diff --git a/assets/images/razorpay.png b/assets/images/razorpay.png new file mode 100644 index 0000000..79042a7 Binary files /dev/null and b/assets/images/razorpay.png differ diff --git a/assets/images/redeem_coupon.png b/assets/images/redeem_coupon.png new file mode 100644 index 0000000..551a59a Binary files /dev/null and b/assets/images/redeem_coupon.png differ diff --git a/assets/images/reedem.png b/assets/images/reedem.png new file mode 100644 index 0000000..789a5aa Binary files /dev/null and b/assets/images/reedem.png differ diff --git a/assets/images/refer_friend.png b/assets/images/refer_friend.png new file mode 100644 index 0000000..4e928ec Binary files /dev/null and b/assets/images/refer_friend.png differ diff --git a/assets/images/referal_top.svg b/assets/images/referal_top.svg new file mode 100644 index 0000000..527cd45 --- /dev/null +++ b/assets/images/referal_top.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simmer_gif.gif b/assets/images/simmer_gif.gif new file mode 100644 index 0000000..1e61f4d Binary files /dev/null and b/assets/images/simmer_gif.gif differ diff --git a/assets/images/story_bg.png b/assets/images/story_bg.png new file mode 100644 index 0000000..4202b3c Binary files /dev/null and b/assets/images/story_bg.png differ diff --git a/assets/images/stripe.png b/assets/images/stripe.png new file mode 100644 index 0000000..74694c4 Binary files /dev/null and b/assets/images/stripe.png differ diff --git a/assets/images/user_placeholder.png b/assets/images/user_placeholder.png new file mode 100644 index 0000000..c60be89 Binary files /dev/null and b/assets/images/user_placeholder.png differ diff --git a/assets/images/wallet.png b/assets/images/wallet.png new file mode 100644 index 0000000..856aff9 Binary files /dev/null and b/assets/images/wallet.png differ diff --git a/assets/images/xendit.png b/assets/images/xendit.png new file mode 100644 index 0000000..64ec01d Binary files /dev/null and b/assets/images/xendit.png differ diff --git a/assets/loader.gif b/assets/loader.gif new file mode 100644 index 0000000..8beb7f8 Binary files /dev/null and b/assets/loader.gif differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..2bc8e05 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,4 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: + - provider: true \ No newline at end of file diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..88cc8d5 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"fondexuzb","appId":"1:893074789710:android:05002c15a64cf1e0c4ba1f","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"fondexuzb","configurations":{"android":"1:893074789710:android:05002c15a64cf1e0c4ba1f","ios":"1:893074789710:ios:510ebf9e4ebed6a8c4ba1f"}}},"ios":{"default":{"projectId":"fondexuzb","appId":"1:893074789710:ios:510ebf9e4ebed6a8c4ba1f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}}}}} \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..0d14080 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 15.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..6649374 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..d3ba75c --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,1812 @@ +PODS: + - abseil/algorithm (1.20240722.0): + - abseil/algorithm/algorithm (= 1.20240722.0) + - abseil/algorithm/container (= 1.20240722.0) + - abseil/algorithm/algorithm (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/algorithm/container (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base (1.20240722.0): + - abseil/base/atomic_hook (= 1.20240722.0) + - abseil/base/base (= 1.20240722.0) + - abseil/base/base_internal (= 1.20240722.0) + - abseil/base/config (= 1.20240722.0) + - abseil/base/core_headers (= 1.20240722.0) + - abseil/base/cycleclock_internal (= 1.20240722.0) + - abseil/base/dynamic_annotations (= 1.20240722.0) + - abseil/base/endian (= 1.20240722.0) + - abseil/base/errno_saver (= 1.20240722.0) + - abseil/base/fast_type_id (= 1.20240722.0) + - abseil/base/log_severity (= 1.20240722.0) + - abseil/base/malloc_internal (= 1.20240722.0) + - abseil/base/no_destructor (= 1.20240722.0) + - abseil/base/nullability (= 1.20240722.0) + - abseil/base/poison (= 1.20240722.0) + - abseil/base/prefetch (= 1.20240722.0) + - abseil/base/pretty_function (= 1.20240722.0) + - abseil/base/raw_logging_internal (= 1.20240722.0) + - abseil/base/spinlock_wait (= 1.20240722.0) + - abseil/base/strerror (= 1.20240722.0) + - abseil/base/throw_delegate (= 1.20240722.0) + - abseil/base/atomic_hook (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/base (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/cycleclock_internal + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/base_internal (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/config (1.20240722.0): + - abseil/xcprivacy + - abseil/base/core_headers (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/cycleclock_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/xcprivacy + - abseil/base/dynamic_annotations (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/endian (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/errno_saver (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/fast_type_id (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/log_severity (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/malloc_internal (1.20240722.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/base/no_destructor (1.20240722.0): + - abseil/base/config + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/nullability (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/poison (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/xcprivacy + - abseil/base/prefetch (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/pretty_function (1.20240722.0): + - abseil/xcprivacy + - abseil/base/raw_logging_internal (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/xcprivacy + - abseil/base/spinlock_wait (1.20240722.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/strerror (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/throw_delegate (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/cleanup/cleanup_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/common (1.20240722.0): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/xcprivacy + - abseil/container/common_policy_traits (1.20240722.0): + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/compressed_tuple (1.20240722.0): + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/container_memory (1.20240722.0): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/fixed_array (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_map (1.20240722.0): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_container_defaults + - abseil/container/raw_hash_map + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/flat_hash_set (1.20240722.0): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_container_defaults + - abseil/container/raw_hash_set + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hash_container_defaults (1.20240722.0): + - abseil/base/config + - abseil/container/hash_function_defaults + - abseil/xcprivacy + - abseil/container/hash_function_defaults (1.20240722.0): + - abseil/base/config + - abseil/container/common + - abseil/hash/hash + - abseil/meta/type_traits + - abseil/strings/cord + - abseil/strings/strings + - abseil/xcprivacy + - abseil/container/hash_policy_traits (1.20240722.0): + - abseil/container/common_policy_traits + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hashtable_debug_hooks (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/container/hashtablez_sampler (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/inlined_vector (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/inlined_vector_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/xcprivacy + - abseil/container/layout (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/demangle_internal + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/raw_hash_map (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/xcprivacy + - abseil/container/raw_hash_set (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/hash/hash + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/crc/cpu_detect (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/crc32c (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/crc/cpu_detect + - abseil/crc/crc_internal + - abseil/crc/non_temporal_memcpy + - abseil/strings/str_format + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_cord_state (1.20240722.0): + - abseil/base/config + - abseil/base/no_destructor + - abseil/crc/crc32c + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/crc_internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/crc/cpu_detect + - abseil/memory/memory + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/non_temporal_arm_intrinsics (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/non_temporal_memcpy (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/crc/non_temporal_arm_intrinsics + - abseil/xcprivacy + - abseil/debugging/bounded_utf8_length_sequence (1.20240722.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/debugging/debugging_internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/debugging/decode_rust_punycode (1.20240722.0): + - abseil/base/config + - abseil/base/nullability + - abseil/debugging/bounded_utf8_length_sequence + - abseil/debugging/utf8_for_code_point + - abseil/xcprivacy + - abseil/debugging/demangle_internal (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/debugging/demangle_rust + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/debugging/demangle_rust (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/decode_rust_punycode + - abseil/xcprivacy + - abseil/debugging/examine_stack (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/xcprivacy + - abseil/debugging/stacktrace (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/xcprivacy + - abseil/debugging/symbolize (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/debugging/utf8_for_code_point (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/flags/commandlineflag (1.20240722.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/commandlineflag_internal (1.20240722.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/xcprivacy + - abseil/flags/config (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/flags/program_name + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/flag (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/commandlineflag + - abseil/flags/config + - abseil/flags/flag_internal + - abseil/flags/reflection + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/flag_internal (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/marshalling + - abseil/flags/reflection + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/xcprivacy + - abseil/flags/marshalling (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/numeric/int128 + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/path_util (1.20240722.0): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/private_handle_accessor (1.20240722.0): + - abseil/base/config + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/program_name (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/reflection (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/container/flat_hash_map + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/private_handle_accessor + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/functional/any_invocable (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/bind_front (1.20240722.0): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/function_ref (1.20240722.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/functional/any_invocable + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/hash/city (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/xcprivacy + - abseil/hash/hash (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/functional/function_ref + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/hash/low_level_hash (1.20240722.0): + - abseil/base/config + - abseil/base/endian + - abseil/base/prefetch + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/log/absl_check (1.20240722.0): + - abseil/log/internal/check_impl + - abseil/xcprivacy + - abseil/log/absl_log (1.20240722.0): + - abseil/log/internal/log_impl + - abseil/xcprivacy + - abseil/log/absl_vlog_is_on (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/vlog_config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/check (1.20240722.0): + - abseil/log/internal/check_impl + - abseil/log/internal/check_op + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/globals (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/hash/hash + - abseil/log/internal/vlog_config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/append_truncated (1.20240722.0): + - abseil/base/config + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/check_impl (1.20240722.0): + - abseil/base/core_headers + - abseil/log/internal/check_op + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/internal/check_op (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/nullguard + - abseil/log/internal/nullstream + - abseil/log/internal/strip + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/conditions (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/voidify + - abseil/xcprivacy + - abseil/log/internal/config (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/log/internal/fnmatch (1.20240722.0): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/format (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/append_truncated + - abseil/log/internal/config + - abseil/log/internal/globals + - abseil/strings/str_format + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/globals (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/strings/strings + - abseil/time/time + - abseil/xcprivacy + - abseil/log/internal/log_impl (1.20240722.0): + - abseil/log/absl_vlog_is_on + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/internal/log_message (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/examine_stack + - abseil/log/globals + - abseil/log/internal/append_truncated + - abseil/log/internal/format + - abseil/log/internal/globals + - abseil/log/internal/log_sink_set + - abseil/log/internal/nullguard + - abseil/log/internal/proto + - abseil/log/log_entry + - abseil/log/log_sink + - abseil/log/log_sink_registry + - abseil/memory/memory + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/log_sink_set (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/cleanup/cleanup + - abseil/log/globals + - abseil/log/internal/config + - abseil/log/internal/globals + - abseil/log/log_entry + - abseil/log/log_sink + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/nullguard (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/log/internal/nullstream (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/proto (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/strip (1.20240722.0): + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/log_message + - abseil/log/internal/nullstream + - abseil/xcprivacy + - abseil/log/internal/vlog_config (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/log/internal/fnmatch + - abseil/memory/memory + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/types/optional + - abseil/xcprivacy + - abseil/log/internal/voidify (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/log/log (1.20240722.0): + - abseil/log/internal/log_impl + - abseil/log/vlog_is_on + - abseil/xcprivacy + - abseil/log/log_entry (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/config + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/log_sink (1.20240722.0): + - abseil/base/config + - abseil/log/log_entry + - abseil/xcprivacy + - abseil/log/log_sink_registry (1.20240722.0): + - abseil/base/config + - abseil/log/internal/log_sink_set + - abseil/log/log_sink + - abseil/xcprivacy + - abseil/log/vlog_is_on (1.20240722.0): + - abseil/log/absl_vlog_is_on + - abseil/xcprivacy + - abseil/memory (1.20240722.0): + - abseil/memory/memory (= 1.20240722.0) + - abseil/memory/memory (1.20240722.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/meta (1.20240722.0): + - abseil/meta/type_traits (= 1.20240722.0) + - abseil/meta/type_traits (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/bits (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/int128 (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/types/compare + - abseil/xcprivacy + - abseil/numeric/representation (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/profiling/exponential_biased (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/profiling/sample_recorder (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/xcprivacy + - abseil/random/bit_gen_ref (1.20240722.0): + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/random + - abseil/xcprivacy + - abseil/random/distributions (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/xcprivacy + - abseil/random/internal/distribution_caller (1.20240722.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/xcprivacy + - abseil/random/internal/fast_uniform_bits (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/fastmath (1.20240722.0): + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/random/internal/generate_real (1.20240722.0): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/iostream_state_saver (1.20240722.0): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/nonsecure_base (1.20240722.0): + - abseil/base/core_headers + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/pcg_engine (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/xcprivacy + - abseil/random/internal/platform (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/internal/pool_urbg (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/randen (1.20240722.0): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/xcprivacy + - abseil/random/internal/randen_engine (1.20240722.0): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes (1.20240722.0): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes_impl (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/randen_slow (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/salted_seed_seq (1.20240722.0): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/seed_material (1.20240722.0): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/traits (1.20240722.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/uniform_helper (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/wide_multiply (1.20240722.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/random (1.20240722.0): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/xcprivacy + - abseil/random/seed_gen_exception (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/seed_sequences (1.20240722.0): + - abseil/base/config + - abseil/base/nullability + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/status/status (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/memory/memory + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/status/statusor (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/has_ostream_operator + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/charset (1.20240722.0): + - abseil/base/core_headers + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/strings/cord (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/crc/crc32c + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/strings + - abseil/types/compare + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cord_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_functions (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/xcprivacy + - abseil/strings/cordz_handle (1.20240722.0): + - abseil/base/config + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/strings/cordz_info (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_statistics (1.20240722.0): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_scope (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_tracker (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/has_ostream_operator (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/strings/str_format (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/strings/str_format_internal + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/str_format_internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/string_view (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/xcprivacy + - abseil/strings/strings (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/charset + - abseil/strings/internal + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/synchronization/graphcycles_internal (1.20240722.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/synchronization/kernel_timeout_internal (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/synchronization/synchronization (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/time (1.20240722.0): + - abseil/time/internal (= 1.20240722.0) + - abseil/time/time (= 1.20240722.0) + - abseil/time/internal (1.20240722.0): + - abseil/time/internal/cctz (= 1.20240722.0) + - abseil/time/internal/cctz (1.20240722.0): + - abseil/time/internal/cctz/civil_time (= 1.20240722.0) + - abseil/time/internal/cctz/time_zone (= 1.20240722.0) + - abseil/time/internal/cctz/civil_time (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/time/internal/cctz/time_zone (1.20240722.0): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/xcprivacy + - abseil/time/time (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types/optional + - abseil/xcprivacy + - abseil/types (1.20240722.0): + - abseil/types/any (= 1.20240722.0) + - abseil/types/bad_any_cast (= 1.20240722.0) + - abseil/types/bad_any_cast_impl (= 1.20240722.0) + - abseil/types/bad_optional_access (= 1.20240722.0) + - abseil/types/bad_variant_access (= 1.20240722.0) + - abseil/types/compare (= 1.20240722.0) + - abseil/types/optional (= 1.20240722.0) + - abseil/types/span (= 1.20240722.0) + - abseil/types/variant (= 1.20240722.0) + - abseil/types/any (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/bad_any_cast (1.20240722.0): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/xcprivacy + - abseil/types/bad_any_cast_impl (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_optional_access (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_variant_access (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/compare (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/optional (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/span (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/variant (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/utility/utility (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/xcprivacy (1.20240722.0) + - Alamofire (5.10.2) + - AppAuth (2.0.0): + - AppAuth/Core (= 2.0.0) + - AppAuth/ExternalUserAgent (= 2.0.0) + - AppAuth/Core (2.0.0) + - AppAuth/ExternalUserAgent (2.0.0): + - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - BoringSSL-GRPC (0.0.37): + - BoringSSL-GRPC/Implementation (= 0.0.37) + - BoringSSL-GRPC/Interface (= 0.0.37) + - BoringSSL-GRPC/Implementation (0.0.37): + - BoringSSL-GRPC/Interface (= 0.0.37) + - BoringSSL-GRPC/Interface (0.0.37) + - camera_avfoundation (0.0.1): + - Flutter + - cloud_firestore (6.1.0): + - Firebase/Firestore (= 12.4.0) + - firebase_core + - Flutter + - Firebase/Auth (12.4.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 12.4.0) + - Firebase/CoreOnly (12.4.0): + - FirebaseCore (~> 12.4.0) + - Firebase/Database (12.4.0): + - Firebase/CoreOnly + - FirebaseDatabase (~> 12.4.0) + - Firebase/Firestore (12.4.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 12.4.0) + - Firebase/Messaging (12.4.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 12.4.0) + - Firebase/Storage (12.4.0): + - Firebase/CoreOnly + - FirebaseStorage (~> 12.4.0) + - firebase_auth (6.1.2): + - Firebase/Auth (= 12.4.0) + - firebase_core + - Flutter + - firebase_core (4.2.1): + - Firebase/CoreOnly (= 12.4.0) + - Flutter + - firebase_database (12.1.0): + - Firebase/Database (= 12.4.0) + - firebase_core + - Flutter + - firebase_messaging (16.0.4): + - Firebase/Messaging (= 12.4.0) + - firebase_core + - Flutter + - firebase_storage (13.0.4): + - Firebase/Storage (= 12.4.0) + - firebase_core + - Flutter + - FirebaseAppCheckInterop (12.4.0) + - FirebaseAuth (12.4.0): + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseAuthInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 6.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (12.4.0) + - FirebaseCore (12.4.0): + - FirebaseCoreInternal (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseCoreInternal (12.4.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseDatabase (12.4.0): + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseSharedSwift (~> 12.4.0) + - GoogleUtilities/UserDefaults (~> 8.1) + - leveldb-library (~> 1.22) + - FirebaseFirestore (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - FirebaseFirestoreInternal (~> 12.4.0) + - FirebaseSharedSwift (~> 12.4.0) + - FirebaseFirestoreInternal (12.4.0): + - abseil/algorithm (~> 1.20240722.0) + - abseil/base (~> 1.20240722.0) + - abseil/container/flat_hash_map (~> 1.20240722.0) + - abseil/memory (~> 1.20240722.0) + - abseil/meta (~> 1.20240722.0) + - abseil/strings/strings (~> 1.20240722.0) + - abseil/time (~> 1.20240722.0) + - abseil/types (~> 1.20240722.0) + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - "gRPC-C++ (~> 1.69.0)" + - gRPC-Core (~> 1.69.0) + - leveldb-library (~> 1.22) + - nanopb (~> 3.30910.0) + - FirebaseInstallations (12.4.0): + - FirebaseCore (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseInstallations (~> 12.4.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - FirebaseSharedSwift (12.4.0) + - FirebaseStorage (12.4.0): + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseAuthInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 6.0, >= 3.4) + - Flutter (1.0.0) + - flutter_local_notifications (0.0.1): + - Flutter + - flutter_osm_plugin (0.0.1): + - Alamofire + - Flutter + - OSMFlutterFramework + - Polyline + - Yams + - fluttertoast (0.0.2): + - Flutter + - geocoding_ios (1.0.5): + - Flutter + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - Google-Maps-iOS-Utils (6.1.0): + - GoogleMaps (~> 9.0) + - google_maps_flutter_ios (0.0.1): + - Flutter + - Google-Maps-iOS-Utils (< 7.0, >= 5.0) + - GoogleMaps (< 10.0, >= 8.4) + - google_sign_in_ios (0.0.1): + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 9.0) + - GTMSessionFetcher (>= 3.4.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMaps (9.4.0): + - GoogleMaps/Maps (= 9.4.0) + - GoogleMaps/Maps (9.4.0) + - GoogleSignIn (9.0.0): + - AppAuth (~> 2.0) + - AppCheckCore (~> 11.0) + - GTMAppAuth (~> 5.0) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - "gRPC-C++ (1.69.0)": + - "gRPC-C++/Implementation (= 1.69.0)" + - "gRPC-C++/Interface (= 1.69.0)" + - "gRPC-C++/Implementation (1.69.0)": + - abseil/algorithm/container (~> 1.20240722.0) + - abseil/base/base (~> 1.20240722.0) + - abseil/base/config (~> 1.20240722.0) + - abseil/base/core_headers (~> 1.20240722.0) + - abseil/base/log_severity (~> 1.20240722.0) + - abseil/base/no_destructor (~> 1.20240722.0) + - abseil/cleanup/cleanup (~> 1.20240722.0) + - abseil/container/flat_hash_map (~> 1.20240722.0) + - abseil/container/flat_hash_set (~> 1.20240722.0) + - abseil/container/inlined_vector (~> 1.20240722.0) + - abseil/flags/flag (~> 1.20240722.0) + - abseil/flags/marshalling (~> 1.20240722.0) + - abseil/functional/any_invocable (~> 1.20240722.0) + - abseil/functional/bind_front (~> 1.20240722.0) + - abseil/functional/function_ref (~> 1.20240722.0) + - abseil/hash/hash (~> 1.20240722.0) + - abseil/log/absl_check (~> 1.20240722.0) + - abseil/log/absl_log (~> 1.20240722.0) + - abseil/log/check (~> 1.20240722.0) + - abseil/log/globals (~> 1.20240722.0) + - abseil/log/log (~> 1.20240722.0) + - abseil/memory/memory (~> 1.20240722.0) + - abseil/meta/type_traits (~> 1.20240722.0) + - abseil/numeric/bits (~> 1.20240722.0) + - abseil/random/bit_gen_ref (~> 1.20240722.0) + - abseil/random/distributions (~> 1.20240722.0) + - abseil/random/random (~> 1.20240722.0) + - abseil/status/status (~> 1.20240722.0) + - abseil/status/statusor (~> 1.20240722.0) + - abseil/strings/cord (~> 1.20240722.0) + - abseil/strings/str_format (~> 1.20240722.0) + - abseil/strings/strings (~> 1.20240722.0) + - abseil/synchronization/synchronization (~> 1.20240722.0) + - abseil/time/time (~> 1.20240722.0) + - abseil/types/optional (~> 1.20240722.0) + - abseil/types/span (~> 1.20240722.0) + - abseil/types/variant (~> 1.20240722.0) + - abseil/utility/utility (~> 1.20240722.0) + - "gRPC-C++/Interface (= 1.69.0)" + - "gRPC-C++/Privacy (= 1.69.0)" + - gRPC-Core (= 1.69.0) + - "gRPC-C++/Interface (1.69.0)" + - "gRPC-C++/Privacy (1.69.0)" + - gRPC-Core (1.69.0): + - gRPC-Core/Implementation (= 1.69.0) + - gRPC-Core/Interface (= 1.69.0) + - gRPC-Core/Implementation (1.69.0): + - abseil/algorithm/container (~> 1.20240722.0) + - abseil/base/base (~> 1.20240722.0) + - abseil/base/config (~> 1.20240722.0) + - abseil/base/core_headers (~> 1.20240722.0) + - abseil/base/log_severity (~> 1.20240722.0) + - abseil/base/no_destructor (~> 1.20240722.0) + - abseil/cleanup/cleanup (~> 1.20240722.0) + - abseil/container/flat_hash_map (~> 1.20240722.0) + - abseil/container/flat_hash_set (~> 1.20240722.0) + - abseil/container/inlined_vector (~> 1.20240722.0) + - abseil/flags/flag (~> 1.20240722.0) + - abseil/flags/marshalling (~> 1.20240722.0) + - abseil/functional/any_invocable (~> 1.20240722.0) + - abseil/functional/bind_front (~> 1.20240722.0) + - abseil/functional/function_ref (~> 1.20240722.0) + - abseil/hash/hash (~> 1.20240722.0) + - abseil/log/check (~> 1.20240722.0) + - abseil/log/globals (~> 1.20240722.0) + - abseil/log/log (~> 1.20240722.0) + - abseil/memory/memory (~> 1.20240722.0) + - abseil/meta/type_traits (~> 1.20240722.0) + - abseil/numeric/bits (~> 1.20240722.0) + - abseil/random/bit_gen_ref (~> 1.20240722.0) + - abseil/random/distributions (~> 1.20240722.0) + - abseil/random/random (~> 1.20240722.0) + - abseil/status/status (~> 1.20240722.0) + - abseil/status/statusor (~> 1.20240722.0) + - abseil/strings/cord (~> 1.20240722.0) + - abseil/strings/str_format (~> 1.20240722.0) + - abseil/strings/strings (~> 1.20240722.0) + - abseil/synchronization/synchronization (~> 1.20240722.0) + - abseil/time/time (~> 1.20240722.0) + - abseil/types/optional (~> 1.20240722.0) + - abseil/types/span (~> 1.20240722.0) + - abseil/types/variant (~> 1.20240722.0) + - abseil/utility/utility (~> 1.20240722.0) + - BoringSSL-GRPC (= 0.0.37) + - gRPC-Core/Interface (= 1.69.0) + - gRPC-Core/Privacy (= 1.69.0) + - gRPC-Core/Interface (1.69.0) + - gRPC-Core/Privacy (1.69.0) + - GTMAppAuth (5.0.0): + - AppAuth/Core (~> 2.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core + - image_picker_ios (0.0.1): + - Flutter + - in_app_review (2.0.0): + - Flutter + - leveldb-library (1.22.6) + - location (0.0.1): + - Flutter + - map_launcher (0.0.1): + - Flutter + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - OSMFlutterFramework (0.8.4) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - Polyline (5.1.0) + - PromisesObjC (2.4.0) + - razorpay-pod (1.4.7) + - razorpay_flutter (1.1.10): + - Flutter + - razorpay-pod + - RecaptchaInterop (101.0.0) + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sign_in_with_apple (0.0.1): + - Flutter + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - Stripe (24.23.3): + - StripeApplePay (= 24.23.3) + - StripeCore (= 24.23.3) + - StripePayments (= 24.23.3) + - StripePaymentsUI (= 24.23.3) + - StripeUICore (= 24.23.3) + - stripe_ios (0.0.1): + - Flutter + - Stripe (~> 24.23.0) + - stripe_ios/stripe_ios (= 0.0.1) + - stripe_ios/stripe_objc (= 0.0.1) + - StripeApplePay (~> 24.23.0) + - StripeFinancialConnections (~> 24.23.0) + - StripePayments (~> 24.23.0) + - StripePaymentSheet (~> 24.23.0) + - StripePaymentsUI (~> 24.23.0) + - stripe_ios/stripe_ios (0.0.1): + - Flutter + - Stripe (~> 24.23.0) + - stripe_ios/stripe_objc + - StripeApplePay (~> 24.23.0) + - StripeFinancialConnections (~> 24.23.0) + - StripePayments (~> 24.23.0) + - StripePaymentSheet (~> 24.23.0) + - StripePaymentsUI (~> 24.23.0) + - stripe_ios/stripe_objc (0.0.1): + - Flutter + - Stripe (~> 24.23.0) + - StripeApplePay (~> 24.23.0) + - StripeFinancialConnections (~> 24.23.0) + - StripePayments (~> 24.23.0) + - StripePaymentSheet (~> 24.23.0) + - StripePaymentsUI (~> 24.23.0) + - StripeApplePay (24.23.3): + - StripeCore (= 24.23.3) + - StripeCore (24.23.3) + - StripeFinancialConnections (24.23.3): + - StripeCore (= 24.23.3) + - StripeUICore (= 24.23.3) + - StripePayments (24.23.3): + - StripeCore (= 24.23.3) + - StripePayments/Stripe3DS2 (= 24.23.3) + - StripePayments/Stripe3DS2 (24.23.3): + - StripeCore (= 24.23.3) + - StripePaymentSheet (24.23.3): + - StripeApplePay (= 24.23.3) + - StripeCore (= 24.23.3) + - StripePayments (= 24.23.3) + - StripePaymentsUI (= 24.23.3) + - StripePaymentsUI (24.23.3): + - StripeCore (= 24.23.3) + - StripePayments (= 24.23.3) + - StripeUICore (= 24.23.3) + - StripeUICore (24.23.3): + - StripeCore (= 24.23.3) + - url_launcher_ios (0.0.1): + - Flutter + - video_compress (0.3.0): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + - Yams (5.0.6) + +DEPENDENCIES: + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_database (from `.symlinks/plugins/firebase_database/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - firebase_storage (from `.symlinks/plugins/firebase_storage/ios`) + - Flutter (from `Flutter`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_osm_plugin (from `.symlinks/plugins/flutter_osm_plugin/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - geocoding_ios (from `.symlinks/plugins/geocoding_ios/ios`) + - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) + - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - in_app_review (from `.symlinks/plugins/in_app_review/ios`) + - location (from `.symlinks/plugins/location/ios`) + - map_launcher (from `.symlinks/plugins/map_launcher/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - razorpay_flutter (from `.symlinks/plugins/razorpay_flutter/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - stripe_ios (from `.symlinks/plugins/stripe_ios/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) + +SPEC REPOS: + trunk: + - abseil + - Alamofire + - AppAuth + - AppCheckCore + - BoringSSL-GRPC + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseDatabase + - FirebaseFirestore + - FirebaseFirestoreInternal + - FirebaseInstallations + - FirebaseMessaging + - FirebaseSharedSwift + - FirebaseStorage + - Google-Maps-iOS-Utils + - GoogleDataTransport + - GoogleMaps + - GoogleSignIn + - GoogleUtilities + - "gRPC-C++" + - gRPC-Core + - GTMAppAuth + - GTMSessionFetcher + - leveldb-library + - nanopb + - OSMFlutterFramework + - Polyline + - PromisesObjC + - razorpay-pod + - RecaptchaInterop + - Stripe + - StripeApplePay + - StripeCore + - StripeFinancialConnections + - StripePayments + - StripePaymentSheet + - StripePaymentsUI + - StripeUICore + - Yams + +EXTERNAL SOURCES: + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" + cloud_firestore: + :path: ".symlinks/plugins/cloud_firestore/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_database: + :path: ".symlinks/plugins/firebase_database/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + firebase_storage: + :path: ".symlinks/plugins/firebase_storage/ios" + Flutter: + :path: Flutter + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_osm_plugin: + :path: ".symlinks/plugins/flutter_osm_plugin/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + geocoding_ios: + :path: ".symlinks/plugins/geocoding_ios/ios" + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/darwin" + google_maps_flutter_ios: + :path: ".symlinks/plugins/google_maps_flutter_ios/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + in_app_review: + :path: ".symlinks/plugins/in_app_review/ios" + location: + :path: ".symlinks/plugins/location/ios" + map_launcher: + :path: ".symlinks/plugins/map_launcher/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + razorpay_flutter: + :path: ".symlinks/plugins/razorpay_flutter/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sign_in_with_apple: + :path: ".symlinks/plugins/sign_in_with_apple/ios" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + stripe_ios: + :path: ".symlinks/plugins/stripe_ios/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" + +SPEC CHECKSUMS: + abseil: a05cc83bf02079535e17169a73c5be5ba47f714b + Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 + AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508 + camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 + cloud_firestore: 7a6d8a533ec7418a7fe46b3a5dabf55661a5b298 + Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e + firebase_auth: 9225db04db5d8e3b46dc8940e04bc6aec6833e27 + firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594 + firebase_database: 78bceaa66968dd1358d16033c680c067b0b01f02 + firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde + firebase_storage: d558bbfa99449fff39352db83c466c8515fa1afa + FirebaseAppCheckInterop: f734c802f21fe1da0837708f0f9a27218c8a4ed0 + FirebaseAuth: 4a2aed737c84114a9d9b33d11ae1b147d6b94889 + FirebaseAuthInterop: 858e6b754966e70740a4370dd1503dfffe6dbb49 + FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 + FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 + FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 + FirebaseDatabase: 0defdda58cfb6fb8816cf3984baa25bceb69fc6a + FirebaseFirestore: 2a6183381cf7679b1bb000eb76a8e3178e25dee2 + FirebaseFirestoreInternal: 6577a27cd5dc3722b900042527f86d4ea1626134 + FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2 + FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5 + FirebaseSharedSwift: 93426a1de92f19e1199fac5295a4f8df16458daa + FirebaseStorage: 20d6b56fb8a40ebaa03d6a2889fe33dac64adb73 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_osm_plugin: 3fb57665b32a81fcffa57c6206658d3a0d0d1a96 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + geocoding_ios: 33776c9ebb98d037b5e025bb0e7537f6dd19646e + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96 + google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264 + google_sign_in_ios: 205742c688aea0e64db9da03c33121694a365109 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 + GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + "gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8 + gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330 + GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 + leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 + location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8 + map_launcher: 8051ad5783913cafce93f2414c6858f2904fd8df + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + OSMFlutterFramework: 2f1260ac2854d3398b92403f2d5e012b4ca2c620 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + Polyline: 2a1f29f87f8d9b7de868940f4f76deb8c678a5b1 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + razorpay-pod: 7bbc8748a93377d1705f9119b2f947faa9c9e3e1 + razorpay_flutter: 0e98e4fcaae27ad50e011d85f66d85e0a008754a + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + Stripe: a4b6584549bac3698733ed564e55b0b067cfda8a + stripe_ios: b720131bae6025a497bf7bb030e238a1a2781f18 + StripeApplePay: c8b005f1b5a242c455f76b42eda08660e9e33102 + StripeCore: 9a87da02c7fefc9b965218210a0902acd71157ab + StripeFinancialConnections: 331ece5b6446dbf6eac62b94f8e365d0af9e000a + StripePayments: 95ca9bc60eca45f02cd35b125a67a969c9d62ce7 + StripePaymentSheet: 7bc20a0ab10d91acc06dc449ab61bb2cf221b0f4 + StripePaymentsUI: 1590647b3f635a58fb0e7058d86d3e9ff89250d4 + StripeUICore: a63d95d604e70a5895f9d6b09c49a46757e86353 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + video_compress: f2133a07762889d67f0711ac831faa26f956980e + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + Yams: e10dae147f517ed57ecae37c5e8681bdf8fcab65 + +PODFILE CHECKSUM: 53a6aebc29ccee84c41f92f409fc20cd4ca011f1 + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..844309b --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,763 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3F72F51D16E365E16BE0E202 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CFF66583CAB48DFBEE37FE4 /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F797F5FE91985170A633F782 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71BF3B994B45C58838C8E035 /* Pods_Runner.framework */; }; + FA1CB5C1D5BF3C5B54F61798 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EAA8CD46DA08393BEAAD3D4E /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0A4636C25F5B98E8FD0FAFBD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 144453FC2E1CCB9E00641581 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4CFF66583CAB48DFBEE37FE4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 71BF3B994B45C58838C8E035 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A8861DC03F997A50003CBF75 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + C07A1EF1EEC51BFAEFDCECAD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + DFAD71F35E4DB2A67BB494C4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + DFB156C6FCA315BB3672CDFA /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EAA8CD46DA08393BEAAD3D4E /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + F6DD44C2993C9DC5627CF8A2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4092B8B0F7BF49F82E411E68 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3F72F51D16E365E16BE0E202 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F797F5FE91985170A633F782 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 7A3C1A742EB1B094B841B7B9 /* Pods */ = { + isa = PBXGroup; + children = ( + A8861DC03F997A50003CBF75 /* Pods-Runner.debug.xcconfig */, + F6DD44C2993C9DC5627CF8A2 /* Pods-Runner.release.xcconfig */, + 0A4636C25F5B98E8FD0FAFBD /* Pods-Runner.profile.xcconfig */, + DFB156C6FCA315BB3672CDFA /* Pods-RunnerTests.debug.xcconfig */, + C07A1EF1EEC51BFAEFDCECAD /* Pods-RunnerTests.release.xcconfig */, + DFAD71F35E4DB2A67BB494C4 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 8B5A44F9DCC2B72969454791 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 71BF3B994B45C58838C8E035 /* Pods_Runner.framework */, + 4CFF66583CAB48DFBEE37FE4 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + EAA8CD46DA08393BEAAD3D4E /* GoogleService-Info.plist */, + 7A3C1A742EB1B094B841B7B9 /* Pods */, + 8B5A44F9DCC2B72969454791 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 144453FC2E1CCB9E00641581 /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + EC443CBD0162A1567C96DBC8 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 4092B8B0F7BF49F82E411E68 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 7DDFAFE565155B0DECD43A35 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 4D52D756B5C8A39DB71727AA /* [CP] Embed Pods Frameworks */, + F0395DC60A758BCE7F3A30CC /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + FA1CB5C1D5BF3C5B54F61798 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 4D52D756B5C8A39DB71727AA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7DDFAFE565155B0DECD43A35 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + EC443CBD0162A1567C96DBC8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F0395DC60A758BCE7F3A30CC /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 9J9XV5T2VN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Emart Customer"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.emart.customer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DFB156C6FCA315BB3672CDFA /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.emart.customer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C07A1EF1EEC51BFAEFDCECAD /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.emart.customer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DFAD71F35E4DB2A67BB494C4 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.emart.customer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 9J9XV5T2VN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Emart Customer"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.emart.customer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 9J9XV5T2VN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Emart Customer"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.emart.customer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..cc75d4c --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,15 @@ +import Flutter +import UIKit +import GoogleMaps + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GMSServices.provideAPIKey("YOUR_API_KEY_HERE") + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png new file mode 100644 index 0000000..eeb78fa Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png new file mode 100644 index 0000000..eeb78fa Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png new file mode 100644 index 0000000..bb3559a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png new file mode 100644 index 0000000..9cdf922 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png new file mode 100644 index 0000000..49f84fc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png new file mode 100644 index 0000000..4fb59b2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png new file mode 100644 index 0000000..4fb59b2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png new file mode 100644 index 0000000..42f31df Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png new file mode 100644 index 0000000..49f84fc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png new file mode 100644 index 0000000..17c0afc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png new file mode 100644 index 0000000..17c0afc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png new file mode 100644 index 0000000..3fa4f47 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png new file mode 100644 index 0000000..eeb78fa Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png new file mode 100644 index 0000000..3fa4f47 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png new file mode 100644 index 0000000..a9c049e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png new file mode 100644 index 0000000..260eb41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png new file mode 100644 index 0000000..3fa4f47 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png new file mode 100644 index 0000000..d96554e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png new file mode 100644 index 0000000..a9c049e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png new file mode 100644 index 0000000..c231fe8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png new file mode 100644 index 0000000..812adc6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..bd04914 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,134 @@ +{ + "images": [ + { + "filename": "AppIcon@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "AppIcon@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "AppIcon~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" + }, + { + "filename": "AppIcon@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" + }, + { + "filename": "AppIcon-83.5@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" + }, + { + "filename": "AppIcon-40@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "AppIcon-40@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "filename": "AppIcon-40~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" + }, + { + "filename": "AppIcon-40@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "AppIcon-20@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "filename": "AppIcon-20@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "filename": "AppIcon-20~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" + }, + { + "filename": "AppIcon-20@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" + }, + { + "filename": "AppIcon-29.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" + }, + { + "filename": "AppIcon-29@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "AppIcon-29@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "filename": "AppIcon-29~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" + }, + { + "filename": "AppIcon-29@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "AppIcon-60@2x~car.png", + "idiom": "car", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "AppIcon-60@3x~car.png", + "idiom": "car", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "AppIcon~ios-marketing.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" + } + ], + "info": { + "author": "iconkitchen", + "version": 1 + } +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..59544f2 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 893074789710-pv12m4nhe82a4ueg9sb2pgt42r0e5da3.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.893074789710-pv12m4nhe82a4ueg9sb2pgt42r0e5da3 + API_KEY + AIzaSyD6Khoz4y93GCj3mOPi2FoluDipplH1av0 + GCM_SENDER_ID + 893074789710 + PLIST_VERSION + 1 + BUNDLE_ID + com.emart.customer + PROJECT_ID + fondexuzb + STORAGE_BUCKET + fondexuzb.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:893074789710:ios:510ebf9e4ebed6a8c4ba1f + DATABASE_URL + https://fondexuzb-default-rtdb.firebaseio.com + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..b6fcdcb --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,84 @@ + + + + + MinimumOSVersion + 15.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.841077018919-978flnqdu54q5lb5e56od2jr2oha90ll + + + + + GIDClientID + 841077018919-978flnqdu54q5lb5e56od2jr2oha90ll.apps.googleusercontent.com + + NSCameraUsageDescription + This app needs access to camera permission + NSLocationAlwaysAndWhenInUseUsageDescription + This app needs access to location when open and in the background. + NSLocationAlwaysUsageDescription + This app needs access to location when in the background. + NSLocationWhenInUseUsageDescription + This app needs access to location when open. + NSMicrophoneUsageDescription + Used to capture audio for image picker plugin + NSPhotoLibraryUsageDescription + This app needs access to your gallery photo. + UIBackgroundModes + + fetch + remote-notification + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Customer + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + customer + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..80b5221 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.developer.applesignin + + Default + + + diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/constant/assets.dart b/lib/constant/assets.dart new file mode 100644 index 0000000..ecb4091 --- /dev/null +++ b/lib/constant/assets.dart @@ -0,0 +1,21 @@ +class AppAssets { + static const assetFromImagesPath = 'assets/images/'; + static const assetFromIconsPath = 'assets/icons/'; + + //images + static const onBoardingBG = '${assetFromImagesPath}onboarding_bg.png'; + static const imgOnBoarding = '${assetFromImagesPath}img_onboarding.png'; + + + //icons + static const icAppLogo = '${assetFromIconsPath}app_logo.png'; + static const icArrowLeft = '${assetFromIconsPath}ic_arrow_left.png'; + static const icMessage = '${assetFromIconsPath}ic_message.png'; + static const icIndia = '${assetFromIconsPath}ic_india.png'; + static const icArrowsClockwise = '${assetFromIconsPath}ic_arrows_clockwise.png'; + static const icLocation = '${assetFromIconsPath}ic_location.png'; + static const icPickup = '${assetFromIconsPath}ic_pickup.png'; + static const icPlus = '${assetFromIconsPath}plus.svg'; + + +} diff --git a/lib/constant/collection_name.dart b/lib/constant/collection_name.dart new file mode 100644 index 0000000..ff7d710 --- /dev/null +++ b/lib/constant/collection_name.dart @@ -0,0 +1,61 @@ +class CollectionName { + static const String users = "users"; + static const String zone = "zone"; + static const String onBoarding = "on_boarding"; + static const String referral = "referral"; + static const String sections = "sections"; + static const String bookedTable = "booked_table"; + static const String chatDriver = "chat_driver"; + static const String chatStore = "chat_store"; + static const String coupons = "coupons"; + static const String currencies = "currencies"; + static const String documents = "documents"; + static const String documentsVerify = "documents_verify"; + static const String driverPayouts = "driver_payouts"; + static const String dynamicNotification = "dynamic_notification"; + static const String emailTemplates = "email_templates"; + static const String favoriteItem = "favorite_item"; + static const String favoriteVendor = "favorite_vendor"; + static const String giftCards = "gift_cards"; + static const String giftPurchases = "gift_purchases"; + static const String bannerItems = "banner_items"; + static const String notifications = "notifications"; + static const String payouts = "payouts"; + static const String vendorOrders = "vendor_orders"; + static const String reviewAttributes = "review_attributes"; + static const String settings = "settings"; + static const String story = "story"; + static const String tax = "tax"; + static const String vendorAttributes = "vendor_attributes"; + static const String vendorCategories = "vendor_categories"; + static const String vendorProducts = "vendor_products"; + static const String vendors = "vendors"; + static const String wallet = "wallet"; + static const String withdrawMethod = "withdraw_method"; + static const String advertisements = "advertisements"; + static const String cashback = "cashback"; + static const String cashbackRedeem = "cashback_redeem"; + static const String currency = 'currencies'; + static const String brands = 'brands'; + static const String parcelCategory = 'parcel_categories'; + static const String parcelWeight = 'parcel_weight'; + static const String parcelOrders = "parcel_orders"; + static const String vehicleType = 'vehicle_type'; + static const String rides = 'rides'; + static const String popularDestinations = 'popular_destinations'; + static const String providerCategories = 'provider_categories'; + static const String providersServices = 'providers_services'; + static const String favoriteService = 'favorite_service'; + static const String itemsReview = 'items_review'; + static const String providersCoupons = 'providers_coupons'; + static const String providerOrders = 'provider_orders'; + static const String providersWorkers = 'providers_workers'; + static const String promos = 'promos'; + static const String parcelCoupons = 'parcel_coupons'; + static const String rentalVehicleType = 'rental_vehicle_type'; + static const String rentalPackages = 'rental_packages'; + static const String rentalCoupons = 'rental_coupons'; + static const String rentalOrders = 'rental_orders'; + static const String sos = 'SOS'; + static const String complaints = 'complaints'; +} diff --git a/lib/constant/constant.dart b/lib/constant/constant.dart new file mode 100644 index 0000000..4d13a6b --- /dev/null +++ b/lib/constant/constant.dart @@ -0,0 +1,779 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/currency_model.dart'; +import 'package:customer/models/order_model.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/models/zone_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart' as geolocator; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:mailer/mailer.dart'; +import 'package:mailer/smtp_server.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:uuid/uuid.dart'; + +import '../models/cart_product_model.dart'; +import '../models/coupon_model.dart'; +import '../models/email_template_model.dart'; +import '../models/language_model.dart'; +import '../models/mail_setting.dart'; +import '../models/section_model.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; +import '../widget/permission_dialog.dart'; +import 'package:http/http.dart' as http; + +RxList cartItem = [].obs; + +class Constant { + static const userPlaceHolder = "assets/images/user_placeholder.png"; + + static String senderId = ''; + static String jsonNotificationFileURL = ''; + static String appVersion = ''; + static List taxList = []; + static String? country = ""; + static String? selectedMapType = ""; + static String? websiteUrl = ''; + static MailSettings? mailSettings; + static bool isSubscriptionModelApplied = false; + static CurrencyModel? currencyData; + static SectionModel? sectionConstantModel; + static geolocator.Position? currentLocation; + + static String cabServiceType = "cab-service"; + static String parcelServiceType = "parcel-service"; + + static bool isZoneAvailable = false; + static ZoneModel? selectedZone; + static List zoneList = []; + + static List sectionColor = [ + [Color(0xFFFEF8E7), Color(0xFFF7CD59)], + [Color(0xFFEEEBF9), Color(0xFF8F7CD8)], + [Color(0xFFFFF8E5), Color(0xFFF7CD59)], + [Color(0xFFF5E5FF), Color(0xFFCC80FF)], + [Color(0xFFFAEBEB), Color(0xFFDB7474)], + [Color(0xFFE5F9FF), Color(0xFF72DEFF)], + [Color(0xFFEFF5F1), Color(0xFFADCEB7)], + [Color(0xFFEAFBF1), Color(0xFF85E5AE)], + [Color(0xFFE7F8FE), Color(0xFF529DB6)], + [Color(0xFFEEEBF9), Color(0xFF8F7CD8)], + ]; + + static List colorList = [ + Color(0xFFFFBC99), + const Color(0xFFCABDFF), + const Color(0xFFB1E5FC), + const Color(0xFFB5EBCD), + const Color(0xFFFFD88D), + const Color(0xFFCBEBA4), + const Color(0xFFFB9B9B), + const Color(0xFFF8B0ED), + const Color(0xFFAFC6FF), + ]; + + static String userRoleDriver = 'driver'; + static String userRoleCustomer = 'customer'; + static String userRoleVendor = 'vendor'; + + static ShippingAddress selectedLocation = ShippingAddress(); + static UserModel? userModel; + static const globalUrl = "https://Replace_your_domain/"; + + static String mapAPIKey = ""; + static String placeHolderImage = ""; + static String defaultCountryCode = ""; + static String defaultCountry = ""; + + static bool isCashbackActive = false; + static bool isEnableOTPTripStart = false; + static bool isEnableOTPTripStartForRental = false; + static bool isMaintenanceModeForCustomer = false; + + static String distanceType = "km"; + + static String googlePlayLink = ""; + static String appStoreLink = ""; + static String termsAndConditions = ""; + static String privacyPolicy = ""; + static String supportURL = ""; + static String minimumAmountToDeposit = "0.0"; + static String minimumAmountToWithdrawal = "0.0"; + static bool? walletSetting = true; + static bool? storyEnable = true; + static bool? specialDiscountOffer = true; + + static const String orderPlaced = "Order Placed"; + static const String orderAccepted = "Order Accepted"; + static const String orderRejected = "Order Rejected"; + static const String orderCancelled = "Order Cancelled"; + static const String driverPending = "Driver Pending"; + static const String driverRejected = "Driver Rejected"; + static const String driverAccepted = 'Driver Accepted'; + static const String orderShipped = "Order Shipped"; + static const String orderInTransit = "In Transit"; + static const String orderCompleted = "Order Completed"; + + static const String orderAssigned = "Order Assigned"; + static const String orderOngoing = "Order Ongoing"; + static const String bookingPlaced = "booking_placed"; + + static CurrencyModel? currencyModel; + static List? restaurantList = []; + + static String walletTopup = "wallet_topup"; + static String newVendorSignup = "new_vendor_signup"; + static String payoutRequestStatus = "payout_request_status"; + static String payoutRequest = "payout_request"; + + static String newOrderPlaced = "order_placed"; + static String scheduleOrder = "schedule_order"; + static String dineInPlaced = "dinein_placed"; + static String dineInCanceled = "dinein_canceled"; + static String dineinAccepted = "dinein_accepted"; + static String restaurantRejected = "restaurant_rejected"; + static String driverCompleted = "driver_completed"; + static String restaurantAccepted = "restaurant_accepted"; + static String takeawayCompleted = "takeaway_completed"; + static String newParcelBook = "new_parcel_book"; + static String newOnDemandBook = "new_ondemand_book"; + + // static String selectedMapType = 'osm'; + static String? mapType = "google"; + + static String? we = "google"; + + static bool isEnableAdsFeature = true; + static bool isSelfDeliveryFeature = false; + + static double getDoubleVal(dynamic input) { + if (input == null) return 0.1; + if (input is int) return input.toDouble(); + if (input is double) return input; + return 0.1; + } + + static bool checkZoneCheck(double latitude, double longLatitude) { + bool isZoneAvailable = false; + for (var element in Constant.zoneList) { + if (Constant.isPointInPolygon(LatLng(latitude, longLatitude), element.area!)) { + isZoneAvailable = true; + break; + } else { + isZoneAvailable = false; + } + } + return isZoneAvailable; + } + + static String? getZoneId(double latitude, double longLatitude) { + String? zoneId; + for (var element in Constant.zoneList) { + if (Constant.isPointInPolygon(LatLng(latitude, longLatitude), element.area!)) { + zoneId = element.id; + break; + } + } + return zoneId; + } + + static String getReferralCode() { + var rng = Random(); + return (rng.nextInt(900000) + 100000).toString(); // 6 digit + } + + static Future checkPermission({required BuildContext context, required Function() onTap}) async { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + if (permission == LocationPermission.denied) { + ShowToastDialog.showToast("You have to allow location permission to use your location"); + } else if (permission == LocationPermission.deniedForever) { + showDialog( + context: context, + builder: (BuildContext context) { + return const PermissionDialog(); + }, + ); + } else { + onTap(); + } + } + + static bool get isRtl { + final locale = Get.locale ?? Get.deviceLocale ?? const Locale('en'); + return Bidi.isRtlLanguage(locale.languageCode); + } + + static bool isExpire(VendorModel venderModel) { + bool isPlanExpire = false; + if (venderModel.subscriptionPlan?.id != null) { + if (venderModel.subscriptionExpiryDate == null) { + if (venderModel.subscriptionPlan?.expiryDay == '-1') { + isPlanExpire = false; + } else { + isPlanExpire = true; + } + } else { + DateTime expiryDate = venderModel.subscriptionExpiryDate!.toDate(); + isPlanExpire = expiryDate.isBefore(DateTime.now()); + } + } else { + isPlanExpire = true; + } + return isPlanExpire; + } + + static bool isExpireDate({required bool expiryDay, Timestamp? subscriptionExpiryDate}) { + bool isPlanExpire = false; + if (expiryDay == true) { + isPlanExpire = false; + } else { + if (subscriptionExpiryDate != null) { + DateTime expiryDate = subscriptionExpiryDate.toDate(); + isPlanExpire = expiryDate.isBefore(DateTime.now()); + } else { + isPlanExpire = true; + } + } + + return isPlanExpire; + } + + static Future showProgress(String message, bool isDismissible) async { + ShowToastDialog.showLoader("$message "); + } + + static void hideProgress() { + ShowToastDialog.closeLoader(); + } + + static String amountShow({required String? amount}) { + if (currencyModel!.symbolatright == true) { + return "${double.parse(amount.toString()).toStringAsFixed(currencyModel?.decimal ?? 0)} ${currencyModel!.symbol.toString()}"; + } else { + return "${currencyModel!.symbol.toString()} ${amount == null || amount.isEmpty ? "0.0" : double.parse(amount.toString()).toStringAsFixed(currencyModel?.decimal ?? 0)}"; + } + } + + static Color statusColor({required String? status}) { + if (status == orderPlaced) { + return AppThemeData.ecommerce300; + } else if (status == orderAccepted || status == orderCompleted) { + return AppThemeData.success400; + } else if (status == orderRejected) { + return AppThemeData.danger300; + } else { + return AppThemeData.warning300; + } + } + + static Color statusText({required String? status}) { + if (status == orderPlaced) { + return AppThemeData.grey50; + } else if (status == orderAccepted || status == orderCompleted) { + return AppThemeData.grey50; + } else if (status == orderRejected) { + return AppThemeData.grey50; + } else { + return AppThemeData.grey900; + } + } + + static String productCommissionPrice(VendorModel vendorModel, String price) { + String commission = "0"; + if (sectionConstantModel!.adminCommision!.isEnabled == true) { + if (vendorModel.adminCommission == null) { + if (sectionConstantModel!.adminCommision!.commissionType!.toLowerCase() == "Percent".toLowerCase() || + sectionConstantModel!.adminCommision!.commissionType?.toLowerCase() == "Percentage".toLowerCase()) { + commission = (double.parse(price) + (double.parse(price) * double.parse(sectionConstantModel!.adminCommision!.amount.toString()) / 100)).toString(); + } else { + commission = (double.parse(price) + double.parse(sectionConstantModel!.adminCommision!.amount.toString())).toString(); + } + } else { + if (vendorModel.adminCommission!.commissionType!.toLowerCase() == "Percent".toLowerCase() || vendorModel.adminCommission!.commissionType?.toLowerCase() == "Percentage".toLowerCase()) { + commission = (double.parse(price) + (double.parse(price) * double.parse(vendorModel.adminCommission!.amount.toString()) / 100)).toString(); + } else { + commission = (double.parse(price) + double.parse(vendorModel.adminCommission!.amount.toString())).toString(); + } + } + } else { + commission = price; + } + + return commission; + } + + static double calculateTax({String? amount, TaxModel? taxModel}) { + double taxAmount = 0.0; + if (taxModel != null && taxModel.enable == true) { + if (taxModel.type == "fix") { + taxAmount = double.parse(taxModel.tax.toString()); + } else { + taxAmount = (double.parse(amount.toString()) * double.parse(taxModel.tax!.toString())) / 100; + } + } + return taxAmount; + } + + static double calculateDiscount({String? amount, CouponModel? offerModel}) { + double taxAmount = 0.0; + if (offerModel != null) { + if (offerModel.discountType == "Percentage" || offerModel.discountType == "percentage") { + taxAmount = (double.parse(amount.toString()) * double.parse(offerModel.discount.toString())) / 100; + } else { + taxAmount = double.parse(offerModel.discount.toString()); + } + } + return taxAmount; + } + + static String calculateReview({required String? reviewCount, required String? reviewSum}) { + if (0 == double.parse(reviewSum.toString()) && 0 == double.parse(reviewSum.toString())) { + return "0"; + } + return (double.parse(reviewSum.toString()) / double.parse(reviewCount.toString())).toStringAsFixed(1); + } + + static String getUuid() { + return const Uuid().v4(); + } + + static Widget loader() { + return Center(child: CircularProgressIndicator(color: AppThemeData.primary300)); + } + + static Widget showEmptyView({required String message}) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Center(child: Text(message, style: TextStyle(fontFamily: AppThemeData.fontFamily, fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))); + } + + static String maskingString(String documentId, int maskingDigit) { + String maskedDigits = documentId; + for (int i = 0; i < documentId.length - maskingDigit; i++) { + maskedDigits = maskedDigits.replaceFirst(documentId[i], "*"); + } + return maskedDigits; + } + + String? validateRequired(String? value, String type) { + if (value!.isEmpty) { + return '$type required'; + } + return null; + } + + String? validateEmail(String? value) { + String pattern = 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,}))$'; + RegExp regExp = RegExp(pattern); + if (value == null || value.isEmpty) { + return "Email is Required"; + } else if (!regExp.hasMatch(value)) { + return "Invalid Email"; + } else { + return null; + } + } + + static String getDistance({required String lat1, required String lng1, required String lat2, required String lng2}) { + double distance; + double distanceInMeters = Geolocator.distanceBetween(double.parse(lat1), double.parse(lng1), double.parse(lat2), double.parse(lng2)); + if (distanceType == "miles") { + distance = distanceInMeters / 1609; + } else { + distance = distanceInMeters / 1000; + } + return distance.toStringAsFixed(2); + } + + bool hasValidUrl(String? value) { + String pattern = r'(http|https)://[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?'; + RegExp regExp = RegExp(pattern); + if (value == null || value.isEmpty) { + return false; + } else if (!regExp.hasMatch(value)) { + return false; + } + return true; + } + + static Future uploadUserImageToFireStorage(File image, String filePath, String fileName) async { + Reference upload = FirebaseStorage.instance.ref().child('$filePath/$fileName'); + UploadTask uploadTask = upload.putFile(image); + var downloadUrl = await (await uploadTask.whenComplete(() {})).ref.getDownloadURL(); + return downloadUrl.toString(); + } + + static Future makePhoneCall(String phoneNumber) async { + final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber); + await launchUrl(launchUri); + } + + Future launchURL(Uri url) async { + if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch $url'); + } + } + + Future getBytesFromAsset(String path, int width) async { + ByteData data = await rootBundle.load(path); + ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width); + ui.FrameInfo fi = await codec.getNextFrame(); + return (await fi.image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List(); + } + + static Future selectTime(context) async { + FocusScope.of(context).requestFocus(FocusNode()); //remove focus + TimeOfDay? newTime = await showTimePicker(context: context, initialTime: TimeOfDay.now()); + if (newTime != null) { + return newTime; + } + return null; + } + + static Future selectDate(context) async { + DateTime? pickedDate = await showDatePicker( + context: context, + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: AppThemeData.primary300, // header background color + onPrimary: AppThemeData.grey900, // header text color + onSurface: AppThemeData.grey900, // body text color + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: AppThemeData.grey900, // button text color + ), + ), + ), + child: child!, + ); + }, + initialDate: DateTime.now(), + //get today's date + firstDate: DateTime(2000), + //DateTime.now() - not to allow to choose before today. + lastDate: DateTime(2101), + ); + return pickedDate; + } + + static int calculateDifference(DateTime date) { + DateTime now = DateTime.now(); + return DateTime(date.year, date.month, date.day).difference(DateTime(now.year, now.month, now.day)).inDays; + } + + static String timestampToDate(Timestamp timestamp) { + DateTime dateTime = timestamp.toDate(); + return DateFormat('MMM dd,yyyy').format(dateTime); + } + + static String timestampToDateTime(Timestamp timestamp) { + DateTime dateTime = timestamp.toDate(); + return DateFormat('MMM dd,yyyy hh:mm aa').format(dateTime); + } + + static String timestampToDateTime2(Timestamp timestamp) { + DateTime dateTime = timestamp.toDate(); + return DateFormat('EEE MMM d yyyy').format(dateTime); + } + + static String timestampToTime(Timestamp timestamp) { + DateTime dateTime = timestamp.toDate(); + return DateFormat('hh:mm aa').format(dateTime); + } + + static String timestampToDateChat(Timestamp timestamp) { + DateTime dateTime = timestamp.toDate(); + return DateFormat('dd/MM/yyyy').format(dateTime); + } + + static DateTime stringToDate(String openDineTime) { + return DateFormat('HH:mm').parse(DateFormat('HH:mm').format(DateFormat("hh:mm a").parse((Intl.getCurrentLocale() == "en_US") ? openDineTime : openDineTime.toLowerCase()))); + } + + static LanguageModel getLanguage() { + final String user = Preferences.getString(Preferences.languageCodeKey); + Map userMap = jsonDecode(user); + return LanguageModel.fromJson(userMap); + } + + static String orderId({String orderId = ''}) { + return "#$orderId"; + //return "#${(orderId).substring(orderId.length - 10)}"; + } + + static bool isPointInPolygon(LatLng point, List polygon) { + int crossings = 0; + for (int i = 0; i < polygon.length; i++) { + int next = (i + 1) % polygon.length; + if (polygon[i].latitude <= point.latitude && polygon[next].latitude > point.latitude || polygon[i].latitude > point.latitude && polygon[next].latitude <= point.latitude) { + double edgeLong = polygon[next].longitude - polygon[i].longitude; + double edgeLat = polygon[next].latitude - polygon[i].latitude; + double interpol = (point.latitude - polygon[i].latitude) / edgeLat; + if (point.longitude < polygon[i].longitude + interpol * edgeLong) { + crossings++; + } + } + } + return (crossings % 2 != 0); + } + + static final smtpServer = SmtpServer( + mailSettings!.host.toString(), + username: mailSettings!.userName.toString(), + password: mailSettings!.password.toString(), + port: 465, + ignoreBadCertificate: false, + ssl: true, + allowInsecure: true, + ); + + static Future sendMail({String? subject, String? body, bool? isAdmin = false, List? recipients}) async { + // Create our message. + if (mailSettings != null) { + if (isAdmin == true) { + recipients!.add(mailSettings!.userName.toString()); + } + final message = + Message() + ..from = Address(mailSettings!.userName.toString(), mailSettings!.fromName.toString()) + ..recipients = recipients! + ..subject = subject + ..text = body + ..html = body; + + try { + final sendReport = await send(message, smtpServer); + print('Message sent: $sendReport'); + } on MailerException catch (e) { + print(e); + print('Message not sent.'); + for (var p in e.problems) { + print('Problem: ${p.code}: ${p.msg}'); + } + } + } + + // var connection = PersistentConnection(smtpServer); + // + // // Send the first message + // await connection.send(message); + } + + static Uri createCoordinatesUrl(double latitude, double longitude, [String? label]) { + Uri uri; + if (kIsWeb) { + uri = Uri.https('www.google.com', '/maps/search/', {'api': '1', 'query': '$latitude,$longitude'}); + } else if (Platform.isAndroid) { + var query = '$latitude,$longitude'; + if (label != null) query += '($label)'; + uri = Uri(scheme: 'geo', host: '0,0', queryParameters: {'q': query}); + } else if (Platform.isIOS) { + var params = {'ll': '$latitude,$longitude'}; + if (label != null) params['q'] = label; + uri = Uri.https('maps.apple.com', '/', params); + } else { + uri = Uri.https('www.google.com', '/maps/search/', {'api': '1', 'query': '$latitude,$longitude'}); + } + + return uri; + } + + static Future sendOrderEmail({required OrderModel orderModel}) async { + EmailTemplateModel? emailTemplateModel = await FireStoreUtils.getEmailTemplates(newOrderPlaced); + if (emailTemplateModel != null) { + String firstHTML = """ + + + + + + + + + + + + """; + + String newString = emailTemplateModel.message.toString(); + newString = newString.replaceAll("{username}", "${Constant.userModel!.firstName} ${Constant.userModel!.lastName}"); + newString = newString.replaceAll("{orderid}", orderModel.id.toString()); + newString = newString.replaceAll("{date}", DateFormat('yyyy-MM-dd').format(orderModel.createdAt!.toDate())); + newString = newString.replaceAll("{address}", orderModel.address!.getFullAddress()); + newString = newString.replaceAll("{paymentmethod}", orderModel.paymentMethod.toString()); + + double deliveryCharge = 0.0; + double total = 0.0; + double specialDiscount = 0.0; + double discount = 0.0; + double taxAmount = 0.0; + double tipValue = 0.0; + String specialLabel = '(${orderModel.specialDiscount!['special_discount_label']}${orderModel.specialDiscount!['specialType'] == "amount" ? currencyModel!.symbol : "%"})'; + List htmlList = []; + + if (orderModel.deliveryCharge != null) { + deliveryCharge = double.parse(orderModel.deliveryCharge.toString()); + } + if (orderModel.tipAmount != null) { + tipValue = double.parse(orderModel.tipAmount.toString()); + } + for (var element in orderModel.products!) { + if (element.extrasPrice != null && element.extrasPrice!.isNotEmpty && double.parse(element.extrasPrice!) != 0.0) { + total += double.parse(element.quantity.toString()) * double.parse(element.extrasPrice!); + } + total += double.parse(element.quantity.toString()) * double.parse(element.price.toString()); + + List? addon = element.extras; + String extrasDisVal = ''; + for (int i = 0; i < addon!.length; i++) { + extrasDisVal += '${addon[i].toString().replaceAll("\"", "")} ${(i == addon.length - 1) ? "" : ","}'; + } + String product = """ + + + + + + + + + + + """; + htmlList.add(product); + } + + if (orderModel.specialDiscount!.isNotEmpty) { + specialDiscount = double.parse(orderModel.specialDiscount!['special_discount'].toString()); + } + + if (orderModel.couponId != null && orderModel.couponId!.isNotEmpty) { + discount = double.parse(orderModel.discount.toString()); + } + + List taxHtmlList = []; + for (var element in taxList) { + taxAmount = taxAmount + calculateTax(amount: (total - discount - specialDiscount).toString(), taxModel: element); + String taxHtml = + """${element.title}: ${amountShow(amount: calculateTax(amount: (total - discount - specialDiscount).toString(), taxModel: element).toString())}${taxList.indexOf(element) == taxList.length - 1 ? "" : "
"}"""; + taxHtmlList.add(taxHtml); + } + + var totalamount = + orderModel.deliveryCharge == null || orderModel.deliveryCharge!.isEmpty + ? total + taxAmount - discount - specialDiscount + : total + taxAmount + double.parse(orderModel.deliveryCharge!) + double.parse(orderModel.tipAmount!) - discount - specialDiscount; + + newString = newString.replaceAll("{subtotal}", amountShow(amount: total.toString())); + newString = newString.replaceAll("{coupon}", orderModel.couponId.toString()); + newString = newString.replaceAll("{discountamount}", amountShow(amount: orderModel.discount.toString())); + newString = newString.replaceAll("{specialcoupon}", specialLabel); + newString = newString.replaceAll("{specialdiscountamount}", amountShow(amount: specialDiscount.toString())); + newString = newString.replaceAll("{shippingcharge}", amountShow(amount: deliveryCharge.toString())); + newString = newString.replaceAll("{tipamount}", amountShow(amount: tipValue.toString())); + newString = newString.replaceAll("{totalAmount}", amountShow(amount: totalamount.toString())); + + String tableHTML = htmlList.join(); + String lastHTML = "
Product Name
Quantity
Price
Extra Item Price
Total
${element.name}${element.quantity}${amountShow(amount: element.price.toString())}${amountShow(amount: element.extrasPrice.toString())}${amountShow(amount: ((double.parse(element.quantity.toString()) * double.parse(element.extrasPrice!) + (double.parse(element.quantity.toString()) * double.parse(element.price.toString())))).toString())}
${extrasDisVal.isEmpty ? "" : "Extra Item : $extrasDisVal"}
"; + newString = newString.replaceAll("{productdetails}", firstHTML + tableHTML + lastHTML); + newString = newString.replaceAll("{taxdetails}", taxHtmlList.join()); + newString = newString.replaceAll("{newwalletbalance}.", amountShow(amount: Constant.userModel!.walletAmount.toString())); + + String subjectNewString = emailTemplateModel.subject.toString(); + subjectNewString = subjectNewString.replaceAll("{orderid}", orderModel.id.toString()); + await sendMail(subject: subjectNewString, isAdmin: emailTemplateModel.isSendToAdmin, body: newString, recipients: [Constant.userModel!.email]); + } + } + + double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + const R = 6371; // Earth's radius in km + final dLat = _degToRad(lat2 - lat1); + final dLon = _degToRad(lon2 - lon1); + final a = sin(dLat / 2) * sin(dLat / 2) + cos(_degToRad(lat1)) * cos(_degToRad(lat2)) * sin(dLon / 2) * sin(dLon / 2); + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return R * c; + } + + double _degToRad(double deg) => deg * (pi / 180); + + String getTimeInTheMinutes({required double distance}) { + double averageSpeed = 40.0; + double estimatedTime = (distance / averageSpeed) * 60; + return "${estimatedTime.toStringAsFixed(2)} minutes"; + } + + /// Calculate tax amount for a single tax model + static double getTaxValue({required String amount, required TaxModel taxModel}) { + double taxVal = 0.0; + if (taxModel.enable == true) { + if (taxModel.type == "fix") { + taxVal = double.tryParse(taxModel.tax.toString()) ?? 0.0; + } else { + taxVal = (double.tryParse(amount) ?? 0.0) * (double.tryParse(taxModel.tax.toString()) ?? 0.0) / 100; + } + } + return taxVal; + } + + Future getBytesFromUrl(String url, {int width = 100}) async { + try { + final http.Response response = await http.get(Uri.parse(url)); + if (response.statusCode != 200) throw Exception("Failed to load image"); + + final Uint8List bytes = response.bodyBytes; + final ui.Codec codec = await ui.instantiateImageCodec(bytes, targetWidth: width); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + + final ByteData? byteData = await frameInfo.image.toByteData(format: ui.ImageByteFormat.png); + return byteData!.buffer.asUint8List(); + } catch (e) { + print("⚠️ getBytesFromUrl error: $e — using default cab icon"); + final ByteData data = await rootBundle.load('assets/images/ic_cab.png'); + return data.buffer.asUint8List(); + } + } + + // Future getBytesFromUrl(String url, {int width = 100}) async { + // final http.Response response = await http.get(Uri.parse(url)); + // if (response.statusCode != 200) { + // throw Exception("Failed to load image from $url"); + // } + // + // final Uint8List bytes = response.bodyBytes; + // + // // Decode & resize + // final ui.Codec codec = await ui.instantiateImageCodec(bytes, targetWidth: width); + // final ui.FrameInfo frameInfo = await codec.getNextFrame(); + // + // final ByteData? byteData = await frameInfo.image.toByteData(format: ui.ImageByteFormat.png); + // return byteData!.buffer.asUint8List(); + // } +} + +extension StringExtension on String { + String capitalizeString() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; + } +} diff --git a/lib/controllers/0n_demand_payment_controller.dart b/lib/controllers/0n_demand_payment_controller.dart new file mode 100644 index 0000000..9c496e6 --- /dev/null +++ b/lib/controllers/0n_demand_payment_controller.dart @@ -0,0 +1,932 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import '../../models/onprovider_order_model.dart'; +import '../models/wallet_transaction_model.dart'; +import '../payment/xenditModel.dart'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as maths; +import 'package:flutter/material.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; +import '../constant/constant.dart'; +import '../models/payment_model/cod_setting_model.dart'; +import '../models/payment_model/flutter_wave_model.dart'; +import '../models/payment_model/mercado_pago_model.dart'; +import '../models/payment_model/mid_trans.dart'; +import '../models/payment_model/orange_money.dart'; +import '../models/payment_model/pay_fast_model.dart'; +import '../models/payment_model/pay_stack_model.dart'; +import '../models/payment_model/paypal_model.dart'; +import '../models/payment_model/paytm_model.dart'; +import '../models/payment_model/razorpay_model.dart'; +import '../models/payment_model/stripe_model.dart'; +import '../models/payment_model/wallet_setting_model.dart'; +import '../models/payment_model/xendit.dart'; +import '../payment/MercadoPagoScreen.dart'; +import '../payment/PayFastScreen.dart'; +import '../payment/getPaytmTxtToken.dart'; +import '../payment/midtrans_screen.dart'; +import '../payment/orangePayScreen.dart'; +import '../payment/paystack/pay_stack_screen.dart'; +import '../payment/paystack/pay_stack_url_model.dart'; +import '../payment/paystack/paystack_url_genrater.dart'; +import '../payment/stripe_failed_model.dart'; +import '../payment/xenditScreen.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../screen_ui/on_demand_service/on_demand_dashboard_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../service/send_notification.dart'; +import '../themes/app_them_data.dart'; +import '../themes/show_toast_dialog.dart'; +import '../utils/preferences.dart'; +import 'on_demand_dashboard_controller.dart'; + +class OnDemandPaymentController extends GetxController { + Rx onDemandOrderModel = Rx(null); + RxDouble totalAmount = 0.0.obs; + late bool isExtra; + + RxBool isLoading = false.obs; + + RxString selectedPaymentMethod = ''.obs; + RxBool isOrderPlaced = false.obs; + + @override + void onInit() { + super.onInit(); + isLoading.value = true; + final args = Get.arguments as Map; + onDemandOrderModel = args['onDemandOrderModel']; + totalAmount = (args['totalAmount'] as double).obs; + print("payment totalAmount ::::::::: $totalAmount"); + isExtra = args['isExtra']; + + getPaymentSettings(); + } + + Future placeOrder() async { + if (!isExtra) { + // Normal Order + ShowToastDialog.showLoader("Please wait...".tr); + + onDemandOrderModel.value?.payment_method = selectedPaymentMethod.value; + onDemandOrderModel.value?.paymentStatus = onDemandOrderModel.value?.provider.priceUnit == "Fixed" && selectedPaymentMethod.value == "cod" ? false : true; + onDemandOrderModel.value?.extraPaymentStatus = true; + + await FireStoreUtils.onDemandOrderPlace(onDemandOrderModel.value!, totalAmount.value); + + if (onDemandOrderModel.value?.status == Constant.orderPlaced) { + await FireStoreUtils.sendOrderOnDemandServiceEmail(orderModel: onDemandOrderModel.value!); + + final providerUser = await FireStoreUtils.getUserProfile(onDemandOrderModel.value!.provider.author!); + + if (providerUser != null) { + final payLoad = {"type": 'provider_order', "orderId": onDemandOrderModel.value?.id}; + await SendNotification.sendFcmMessage(Constant.bookingPlaced, providerUser.fcmToken ?? '', payLoad); + } + + ShowToastDialog.showToast("OnDemand Service successfully booked".tr); + } + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.value.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: onDemandOrderModel.value!.id, + note: "Booking Amount debited".tr, + paymentStatus: "success".tr, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()).then((value) {}); + } + }); + } + + ShowToastDialog.closeLoader(); + Get.offAll(const OnDemandDashboardScreen()); + OnDemandDashboardController controller = Get.put(OnDemandDashboardController()); + controller.selectedIndex.value = 2; + } else { + // Extra Charges Flow + onDemandOrderModel.value?.createdAt = Timestamp.now(); + onDemandOrderModel.value?.extraPaymentStatus = true; + + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.value.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: onDemandOrderModel.value!.id, + note: "Booking Extra charge debited", + paymentStatus: "success".tr, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()).then((value) {}); + } + }); + } + + // Handle wallet payment if needed + if (selectedPaymentMethod.value != 'cod') { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + serviceType: 'ondemand-service', + amount: totalAmount.value, + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "provider", + userId: onDemandOrderModel.value?.provider.author!, + isTopup: true, + orderId: onDemandOrderModel.value?.id, + note: 'Extra Charge Amount Credited', + paymentStatus: "success".tr, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-$totalAmount", userId: FireStoreUtils.getCurrentUid()); + } + }); + } + + await FireStoreUtils.updateOnDemandOrder(onDemandOrderModel.value!); + + ShowToastDialog.closeLoader(); + Get.offAll(const OnDemandDashboardScreen()); + OnDemandDashboardController controller = Get.put(OnDemandDashboardController()); + controller.selectedIndex.value = 2; + } + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + isLoading.value = true; + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'eMart Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + isLoading.value = false; + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": Constant.userModel?.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": Constant.userModel?.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + placeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen( + amount: (double.parse(totalAmount) * 100).toString(), + currency: "ZAR", + secretKey: payStackModel.value.secretKey.toString(), + userModel: Constant.userModel!, + ).then((value) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": {"email": Constant.userModel?.email.toString(), "phonenumber": Constant.userModel?.phoneNumber, "name": Constant.userModel?.fullName()}, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: Constant.userModel!).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + ); + + final data = jsonDecode(response.body); + await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + initiatePayment(amount: amount, orderId: orderId).then((value) { + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + + GetPaymentTxtTokenModel result = value; + startTransaction(context, txnTokenBy: result.body.txnToken ?? '', orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + }); + }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // placeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), "checksum_value": checkSum}, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post( + Uri.parse(initiateURL), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }, + ); + log(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': Constant.userModel?.phoneNumber, 'email': Constant.userModel?.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + ///Orangepay payment + + // static variables to store transaction session + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + + ShowToastDialog.closeLoader(); + + if (paymentURL.toString().isNotEmpty) { + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth ?? ''}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token'] ?? ''; + if (accessToken.isEmpty) { + ShowToastDialog.showToast("Failed to get access token".tr); + return ''; + } + + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + + // ✅ Null-safe handling + bool isSandbox = orangeMoneyModel.value.isSandbox ?? false; + + String apiUrl = isSandbox ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": isSandbox ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl ?? "", + "cancel_url": orangeMoneyModel.value.cancelUrl ?? "", + "notif_url": orangeMoneyModel.value.notifUrl ?? "", + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token'] ?? ''; + return responseData['payment_url'] ?? ''; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + // static String accessToken = ''; + // static String payToken = ''; + // static String orderId = ''; + // static String amount = ''; + // + // Future orangeMakePayment({required String amount, required BuildContext context}) async { + // reset(); + // var id = const Uuid().v4(); + // var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + // ShowToastDialog.closeLoader(); + // if (paymentURL.toString() != '') { + // Get.to( + // () => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken), + // )!.then((value) { + // if (value == true) { + // ShowToastDialog.showToast("Payment Successful!!".tr); + // placeOrder(); + // (); + // } + // }); + // } else { + // ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + // } + // } + // + // Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + // String apiUrl = 'https://api.orange.com/oauth/v3/token'; + // Map requestBody = {'grant_type': 'client_credentials'}; + // + // var response = await http.post( + // Uri.parse(apiUrl), + // headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + // body: requestBody, + // ); + // + // // Handle the response + // + // if (response.statusCode == 200) { + // Map responseData = jsonDecode(response.body); + // + // accessToken = responseData['access_token']; + // // ignore: use_build_context_synchronously + // return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + // } else { + // ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + // return ''; + // } + // } + // + // Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + // orderId = orderIdData; + // amount = amountData; + // String apiUrl = + // orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + // Map requestBody = { + // "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + // "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + // "order_id": orderId, + // "amount": amount, + // "reference": 'Y-Note Test', + // "lang": "en", + // "return_url": orangeMoneyModel.value.returnUrl ?? "", + // "cancel_url": orangeMoneyModel.value.cancelUrl ?? "", + // "notif_url": orangeMoneyModel.value.notifUrl ?? "", + // // "return_url": orangeMoneyModel.value.returnUrl!.toString(), + // // "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + // // "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + // }; + // + // var response = await http.post( + // Uri.parse(apiUrl), + // headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + // body: json.encode(requestBody), + // ); + // + // // Handle the response + // if (response.statusCode == 201) { + // Map responseData = jsonDecode(response.body); + // if (responseData['message'] == 'OK') { + // payToken = responseData['pay_token']; + // return responseData['payment_url']; + // } else { + // return ''; + // } + // } else { + // ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + // return ''; + // } + // } + // + // static void reset() { + // accessToken = ''; + // payToken = ''; + // orderId = ''; + // amount = ''; + // } + + ///XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/Intercity_home_controller.dart b/lib/controllers/Intercity_home_controller.dart new file mode 100644 index 0000000..2322405 --- /dev/null +++ b/lib/controllers/Intercity_home_controller.dart @@ -0,0 +1,1605 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:math' as maths; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/collection_name.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cab_order_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/payment_model/cod_setting_model.dart'; +import 'package:customer/models/payment_model/flutter_wave_model.dart'; +import 'package:customer/models/payment_model/mercado_pago_model.dart'; +import 'package:customer/models/payment_model/mid_trans.dart'; +import 'package:customer/models/payment_model/orange_money.dart'; +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:customer/models/payment_model/pay_stack_model.dart'; +import 'package:customer/models/payment_model/paypal_model.dart'; +import 'package:customer/models/payment_model/paytm_model.dart'; +import 'package:customer/models/payment_model/razorpay_model.dart'; +import 'package:customer/models/payment_model/stripe_model.dart'; +import 'package:customer/models/payment_model/wallet_setting_model.dart'; +import 'package:customer/models/payment_model/xendit.dart'; +import 'package:customer/models/popular_destination.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vehicle_type.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/payment/MercadoPagoScreen.dart'; +import 'package:customer/payment/PayFastScreen.dart'; +import 'package:customer/payment/getPaytmTxtToken.dart'; +import 'package:customer/payment/midtrans_screen.dart'; +import 'package:customer/payment/orangePayScreen.dart'; +import 'package:customer/payment/paystack/pay_stack_screen.dart'; +import 'package:customer/payment/paystack/pay_stack_url_model.dart'; +import 'package:customer/payment/paystack/paystack_url_genrater.dart'; +import 'package:customer/payment/stripe_failed_model.dart'; +import 'package:customer/payment/xenditModel.dart'; +import 'package:customer/payment/xenditScreen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:http/http.dart' as http; +import 'package:latlong2/latlong.dart' as latlong; +import 'package:location/location.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; + +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../themes/app_them_data.dart'; + +class IntercityHomeController extends GetxController { + RxList popularDestination = [].obs; + + late GoogleMapController mapController; + final flutterMap.MapController mapOsmController = flutterMap.MapController(); + + final Rx sourceTextEditController = TextEditingController().obs; + final Rx destinationTextEditController = TextEditingController().obs; + + final Rx couponCodeTextEditController = TextEditingController().obs; + + final Rx currentLocation = Location().obs; + + final RxSet markers = {}.obs; + final RxList osmMarker = [].obs; + final RxList routePoints = [].obs; + + final Rx currentPosition = LatLng(23.0225, 72.5714).obs; + + final Rx departureLatLong = const LatLng(0.0, 0.0).obs; + final Rx destinationLatLong = const LatLng(0.0, 0.0).obs; + final Rx departureLatLongOsm = latlong.LatLng(0.0, 0.0).obs; + final Rx destinationLatLongOsm = latlong.LatLng(0.0, 0.0).obs; + + final RxBool isLoading = true.obs; + + final RxDouble distance = 0.0.obs; + final RxString duration = ''.obs; + + BitmapDescriptor? departureIcon, destinationIcon, taxiIcon, stopIcon; + Widget? departureIconOsm, destinationIconOsm, taxiIconOsm, stopIconOsm; + + RxList taxList = [].obs; + RxList vehicleTypes = [].obs; + Rx selectedVehicleType = VehicleType().obs; + + Rx userModel = UserModel().obs; + Rx driverModel = UserModel().obs; + Rx currentOrder = CabOrderModel().obs; + + final RxString selectedPaymentMethod = ''.obs; + final RxString bottomSheetType = 'location'.obs; + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + bool isOsmMapReady = false; + + Rx selectedCouponModel = CouponModel().obs; + + @override + void onInit() { + super.onInit(); + taxList.value = Constant.taxList; + initData(); + } + + Future initData() async { + if (Constant.selectedMapType == 'osm') { + mapOsmController; + } + + await setIcons(); + await FireStoreUtils.getPopularDestination().then((value) { + popularDestination.value = value; + }); + await getVehicleType(); + isLoading.value = false; + } + + RxList cabCouponList = [].obs; + + Future getVehicleType() async { + final vehicleList = await FireStoreUtils.getVehicleType(); + vehicleTypes.value = vehicleList; + if (vehicleTypes.isNotEmpty) { + selectedVehicleType.value = vehicleTypes.first; + } + + await getPaymentSettings(); + + FireStoreUtils.fireStore.collection(CollectionName.users).doc(FireStoreUtils.getCurrentUid()).snapshots().listen((userSnapshot) async { + if (!userSnapshot.exists) return; + + userModel.value = UserModel.fromJson(userSnapshot.data()!); + + if (userModel.value.inProgressOrderID != null && userModel.value.inProgressOrderID!.isNotEmpty) { + String? validRideId; + + for (String id in userModel.value.inProgressOrderID!) { + final rideDoc = await FireStoreUtils.fireStore.collection(CollectionName.rides).doc(id).get(); + + if (rideDoc.exists && (rideDoc.data()?['rideType'] ?? '').toString().toLowerCase() == "intercity") { + validRideId = userModel.value.inProgressOrderID!.first!; + break; + } + } + + FireStoreUtils.fireStore.collection(CollectionName.rides).doc(validRideId).snapshots().listen((rideSnapshot) async { + if (!rideSnapshot.exists) return; + + final rideData = rideSnapshot.data()!; + currentOrder.value = CabOrderModel.fromJson(rideData); + final status = currentOrder.value.status; + + if (status == Constant.driverAccepted || status == Constant.orderInTransit) { + FireStoreUtils.fireStore.collection(CollectionName.users).doc(currentOrder.value.driverId).snapshots().listen((event) async { + if (event.exists && event.data() != null) { + UserModel driverModel0 = UserModel.fromJson(event.data()!); + driverModel.value = driverModel0; + await updateDriverRoute(driverModel0); + } + }); + } + + print("Current Ride Status: $status"); + if (status == Constant.orderPlaced || status == Constant.driverPending || status == Constant.driverRejected || (status == Constant.orderAccepted && currentOrder.value.driverId == null)) { + bottomSheetType.value = 'waitingForDriver'; + } else if (status == Constant.driverAccepted || status == Constant.orderInTransit) { + bottomSheetType.value = 'driverDetails'; + sourceTextEditController.value.text = currentOrder.value.sourceLocationName ?? ''; + destinationTextEditController.value.text = currentOrder.value.destinationLocationName ?? ''; + selectedPaymentMethod.value = currentOrder.value.paymentMethod ?? ''; + calculateTotalAmountAfterAccept(); + } else if (status == Constant.orderCompleted) { + userModel.value.inProgressOrderID!.remove(validRideId); + await FireStoreUtils.updateUser(userModel.value); + bottomSheetType.value = 'location'; + Get.back(); + } + }); + } else { + bottomSheetType.value = 'location'; + if (Constant.currentLocation != null) { + setDepartureMarker(Constant.currentLocation!.latitude, Constant.currentLocation!.longitude); + searchPlaceNameOSM(); + } + } + }); + + final coupons = await FireStoreUtils.getCabCoupon(); + cabCouponList.value = coupons; + } + + Future updateDriverRoute(UserModel driverModel) async { + try { + final order = currentOrder.value; + + final driverLat = driverModel.location!.latitude ?? 0.0; + final driverLng = driverModel.location!.longitude ?? 0.0; + + if (driverLat == 0.0 || driverLng == 0.0) return; + + // Get pickup and destination + final pickupLat = order.sourceLocation?.latitude ?? 0.0; + final pickupLng = order.sourceLocation?.longitude ?? 0.0; + final destLat = order.destinationLocation?.latitude ?? 0.0; + final destLng = order.destinationLocation?.longitude ?? 0.0; + + if (Constant.selectedMapType == 'osm') { + /// For OpenStreetMap + routePoints.clear(); + + if (order.status == Constant.driverAccepted) { + // DRIVER → PICKUP + await fetchRouteWithWaypoints([latlong.LatLng(driverLat, driverLng), latlong.LatLng(pickupLat, pickupLng)]); + } else if (order.status == Constant.orderInTransit) { + // PICKUP → DESTINATION + await fetchRouteWithWaypoints([latlong.LatLng(pickupLat, pickupLng), latlong.LatLng(destLat, destLng)]); + } + updateRouteMarkers(driverModel); + } else { + /// For Google Maps + if (order.status == Constant.driverAccepted) { + await fetchGoogleRouteBetween(LatLng(driverLat, driverLng), LatLng(pickupLat, pickupLng)); + } else if (order.status == Constant.orderInTransit) { + await fetchGoogleRouteBetween(LatLng(pickupLat, pickupLng), LatLng(destLat, destLng)); + } + updateRouteMarkers(driverModel); + } + } catch (e) { + print("Error in updateDriverRoute: $e"); + } + } + + Future updateRouteMarkers(UserModel driverModel) async { + try { + final order = currentOrder.value; + if (order.driver == null || driverModel.location == null) return; + + final driverLat = driverModel.location!.latitude ?? 0.0; + final driverLng = driverModel.location!.longitude ?? 0.0; + final pickupLat = order.sourceLocation?.latitude ?? 0.0; + final pickupLng = order.sourceLocation?.longitude ?? 0.0; + final destLat = order.destinationLocation?.latitude ?? 0.0; + final destLng = order.destinationLocation?.longitude ?? 0.0; + + markers.clear(); + osmMarker.clear(); + + final departureBytes = await Constant().getBytesFromAsset('assets/images/location_black3x.png', 50); + final destinationBytes = await Constant().getBytesFromAsset('assets/images/location_orange3x.png', 50); + final driverBytesRaw = + (Constant.sectionConstantModel?.markerIcon?.isNotEmpty ?? false) + ? await Constant().getBytesFromUrl(Constant.sectionConstantModel!.markerIcon!, width: 120) + : await Constant().getBytesFromAsset('assets/images/ic_cab.png', 50); + + departureIcon = BitmapDescriptor.fromBytes(departureBytes); + destinationIcon = BitmapDescriptor.fromBytes(destinationBytes); + taxiIcon = BitmapDescriptor.fromBytes(driverBytesRaw); + + if (Constant.selectedMapType == 'osm') { + if (order.status == Constant.driverAccepted) { + osmMarker.addAll([ + flutterMap.Marker(point: latlong.LatLng(pickupLat, pickupLng), width: 40, height: 40, child: Image.asset('assets/images/location_black3x.png', width: 40)), + flutterMap.Marker( + point: latlong.LatLng(driverLat, driverLng), + width: 45, + height: 45, + rotate: true, + child: CachedNetworkImage( + width: 50, + height: 50, + imageUrl: Constant.sectionConstantModel!.markerIcon.toString(), + placeholder: (context, url) => Constant.loader(), + errorWidget: (context, url, error) => SizedBox(width: 30, height: 30, child: CircularProgressIndicator(strokeWidth: 2)), + ), + ), + ]); + } else if (order.status == Constant.orderInTransit) { + osmMarker.addAll([ + flutterMap.Marker(point: latlong.LatLng(destLat, destLng), width: 40, height: 40, child: Image.asset('assets/images/location_orange3x.png', width: 40)), + flutterMap.Marker( + point: latlong.LatLng(driverLat, driverLng), + width: 45, + height: 45, + rotate: true, + child: CachedNetworkImage( + width: 50, + height: 50, + imageUrl: Constant.sectionConstantModel!.markerIcon.toString(), + placeholder: (context, url) => Constant.loader(), + errorWidget: (context, url, error) => SizedBox(width: 30, height: 30, child: CircularProgressIndicator(strokeWidth: 2)), + ), + ), + ]); + } + } else { + if (order.status == Constant.driverAccepted) { + markers.addAll([ + Marker( + markerId: const MarkerId("pickup"), + position: LatLng(pickupLat, pickupLng), + infoWindow: InfoWindow(title: "Pickup Location".tr), + icon: departureIcon ?? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), + ), + Marker(markerId: const MarkerId("driver"), position: LatLng(driverLat, driverLng), infoWindow: InfoWindow(title: "Driver at Pickup".tr), icon: taxiIcon ?? BitmapDescriptor.defaultMarker), + ]); + } else if (order.status == Constant.orderInTransit) { + markers.addAll([ + Marker( + markerId: const MarkerId("destination"), + position: LatLng(destLat, destLng), + infoWindow: InfoWindow(title: "Destination Location".tr), + icon: destinationIcon ?? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), + ), + Marker(markerId: const MarkerId("driver"), position: LatLng(driverLat, driverLng), infoWindow: InfoWindow(title: "Driver Location".tr), icon: taxiIcon ?? BitmapDescriptor.defaultMarker), + ]); + } + } + + update(); + } catch (e) { + print("❌ Error in updateRouteMarkers: $e"); + } + } + + Future fetchGoogleRouteBetween(LatLng originPoint, LatLng destPoint) async { + final origin = '${originPoint.latitude},${originPoint.longitude}'; + final destination = '${destPoint.latitude},${destPoint.longitude}'; + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/directions/json' + '?origin=$origin&destination=$destination' + '&mode=driving&key=${Constant.mapAPIKey}', + ); + + try { + final response = await http.get(url); + final data = json.decode(response.body); + + if (data['status'] == 'OK') { + final route = data['routes'][0]; + final encodedPolyline = route['overview_polyline']['points']; + final decodedPoints = PolylinePoints.decodePolyline(encodedPolyline); + final coordinates = decodedPoints.map((e) => LatLng(e.latitude, e.longitude)).toList(); + + addPolyLine(coordinates); + + // Distance + duration update + final leg = route['legs'][0]; + final totalDistance = leg['distance']['value'] / 1000.0; + final totalDuration = leg['duration']['value'] / 60.0; + + distance.value = totalDistance; + duration.value = '${totalDuration.toStringAsFixed(0)} min'; + } else { + print('Google Directions API error: ${data['status']}'); + } + } catch (e) { + print("Error fetching driver route: $e"); + } + } + + void calculateTotalAmountAfterAccept() { + taxAmount = 0.0.obs; + discount = 0.0.obs; + subTotal.value = double.parse(currentOrder.value.subTotal.toString()); + discount.value = double.parse(currentOrder.value.discount ?? '0.0'); + + if (currentOrder.value.taxSetting != null) { + for (var element in currentOrder.value.taxSetting!) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + update(); + } + + void calculateTotalAmount() { + subTotal = 0.0.obs; + taxAmount = 0.0.obs; + discount = 0.0.obs; + totalAmount = 0.0.obs; + subTotal.value = getAmount(selectedVehicleType.value); + + if (selectedCouponModel.value.id != null) { + discount.value = Constant.calculateDiscount(amount: subTotal.value.toString(), offerModel: selectedCouponModel.value); + } + + for (var element in Constant.taxList) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + update(); + } + + Future completeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.cod.name) { + currentOrder.value.paymentMethod = selectedPaymentMethod.value; + await FireStoreUtils.cabOrderPlace(currentOrder.value).then((value) { + ShowToastDialog.showToast("Payment method changed".tr); + Get.back(); + Get.back(); + }); + } else { + currentOrder.value.paymentStatus = true; + currentOrder.value.paymentMethod = selectedPaymentMethod.value; + userModel.value.inProgressOrderID ??= []; + userModel.value.inProgressOrderID!.clear(); + await FireStoreUtils.updateUser(userModel.value); + + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: currentOrder.value.id, + note: "Cab Amount debited".tr, + paymentStatus: "success".tr, + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()); + } + }); + } + + await FireStoreUtils.cabOrderPlace(currentOrder.value).then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + Get.back(); + }); + } + } + + Future placeOrder() async { + DestinationLocation sourceLocation = DestinationLocation( + latitude: Constant.selectedMapType == 'osm' ? departureLatLongOsm.value.latitude : departureLatLong.value.latitude, + longitude: Constant.selectedMapType == 'osm' ? departureLatLongOsm.value.longitude : departureLatLong.value.longitude, + ); + + DestinationLocation destinationLocation = DestinationLocation( + latitude: Constant.selectedMapType == 'osm' ? destinationLatLongOsm.value.latitude : destinationLatLong.value.latitude, + longitude: Constant.selectedMapType == 'osm' ? destinationLatLongOsm.value.longitude : destinationLatLong.value.longitude, + ); + + CabOrderModel orderModel = CabOrderModel(); + orderModel.id = const Uuid().v4(); + orderModel.distance = distance.value.toString(); + orderModel.duration = duration.value; + orderModel.vehicleId = selectedVehicleType.value.id; + orderModel.vehicleType = selectedVehicleType.value; + orderModel.authorID = FireStoreUtils.getCurrentUid(); + orderModel.sourceLocationName = sourceTextEditController.value.text; + orderModel.destinationLocationName = destinationTextEditController.value.text; + + orderModel.sourceLocation = sourceLocation; + orderModel.destinationLocation = destinationLocation; + orderModel.author = userModel.value; + orderModel.subTotal = subTotal.value.toString(); + orderModel.discount = discount.value.toString(); + orderModel.couponCode = selectedCouponModel.value.code; + orderModel.couponId = selectedCouponModel.value.id; + + orderModel.taxSetting = Constant.taxList; + orderModel.adminCommissionType = + Constant.sectionConstantModel!.adminCommision != null && Constant.sectionConstantModel!.adminCommision!.isEnabled == true + ? Constant.sectionConstantModel!.adminCommision!.commissionType.toString() + : null; + orderModel.adminCommission = + Constant.sectionConstantModel!.adminCommision != null && Constant.sectionConstantModel!.adminCommision!.isEnabled == true + ? Constant.sectionConstantModel!.adminCommision!.amount.toString() + : null; + orderModel.couponCode = couponCodeTextEditController.value.text; + orderModel.paymentMethod = selectedPaymentMethod.value; + orderModel.paymentStatus = false; + orderModel.triggerDelevery = Timestamp.now(); + orderModel.tipAmount = "0.0"; + orderModel.scheduleReturnDateTime = Timestamp.now(); + orderModel.rideType = 'intercity'; + orderModel.roundTrip = false; + orderModel.sectionId = Constant.sectionConstantModel!.id; + orderModel.createdAt = Timestamp.now(); + orderModel.otpCode = (maths.Random().nextInt(9000) + 1000).toString(); // Generate a 4-digit OTP + orderModel.status = Constant.orderPlaced; + orderModel.scheduleDateTime = Timestamp.now(); + log("Order Model : ${orderModel.toJson()}"); + + await FireStoreUtils.cabOrderPlace(orderModel); + + userModel.value.inProgressOrderID!.add(orderModel.id); + await FireStoreUtils.updateUser(userModel.value); + + bottomSheetType.value = 'waitingForDriver'; + } + + double getAmount(VehicleType vehicleType) { + final double currentDistance = distance.value; + if (currentDistance <= (vehicleType.minimum_delivery_charges_within_km ?? 0)) { + return double.tryParse(vehicleType.minimum_delivery_charges.toString()) ?? 0.0; + } else { + return (vehicleType.delivery_charges_per_km ?? 0.0) * currentDistance; + } + } + + void setDepartureMarker(double lat, double long) { + if (Constant.selectedMapType == 'osm') { + _setOsmMarker(lat, long, isDeparture: true); + } else { + _setGoogleMarker(lat, long, isDeparture: true); + } + } + + void setDestinationMarker(double lat, double lng) { + if (Constant.selectedMapType == 'osm') { + _setOsmMarker(lat, lng, isDeparture: false); + } else { + _setGoogleMarker(lat, lng, isDeparture: false); + } + } + + void setStopMarker(double lat, double lng, int index) { + if (Constant.selectedMapType == 'osm') { + // Add new stop marker without clearing + osmMarker.add(flutterMap.Marker(point: latlong.LatLng(lat, lng), width: 40, height: 40, child: stopIconOsm!)); + + getDirections(isStopMarker: true); + } else { + final markerId = MarkerId('Stop $index'); + + markers.removeWhere((marker) => marker.markerId == markerId); + markers.add(Marker(markerId: markerId, infoWindow: InfoWindow(title: 'Stop ${String.fromCharCode(index + 65)}'), position: LatLng(lat, lng), icon: stopIcon!)); + + getDirections(); + } + } + + void _setOsmMarker(double lat, double lng, {required bool isDeparture}) { + final marker = flutterMap.Marker(point: latlong.LatLng(lat, lng), width: 40, height: 40, child: isDeparture ? departureIconOsm! : destinationIconOsm!); + if (isDeparture) { + departureLatLongOsm.value = latlong.LatLng(lat, lng); + } else { + destinationLatLongOsm.value = latlong.LatLng(lat, lng); + } + osmMarker.add(marker); + if (departureLatLongOsm.value.latitude != 0 && destinationLatLongOsm.value.latitude != 0) { + getDirections(); + animateToSource(lat, lng); + } + } + + void _setGoogleMarker(double lat, double lng, {required bool isDeparture}) { + final LatLng pos = LatLng(lat, lng); + final markerId = MarkerId(isDeparture ? 'Departure' : 'Destination'); + final icon = isDeparture ? departureIcon! : destinationIcon!; + final title = isDeparture ? 'Departure'.tr : 'Destination'.tr; + + if (isDeparture) { + departureLatLong.value = pos; + } else { + destinationLatLong.value = pos; + } + + // Remove only the matching departure/destination marker + markers.removeWhere((marker) => marker.markerId == markerId); + + // Add new marker + markers.add(Marker(markerId: markerId, position: pos, icon: icon, infoWindow: InfoWindow(title: title))); + + mapController.animateCamera(CameraUpdate.newLatLngZoom(pos, 14)); + + if (departureLatLong.value.latitude != 0 && destinationLatLong.value.latitude != 0) { + getDirections(); + } else { + mapController.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(target: LatLng(lat, lng), zoom: 14))); + } + } + + Future getDirections({bool isStopMarker = false}) async { + if (Constant.selectedMapType == 'osm') { + final wayPoints = []; + + // Only add valid source + if (departureLatLongOsm.value.latitude != 0.0 && departureLatLongOsm.value.longitude != 0.0) { + wayPoints.add(departureLatLongOsm.value); + } + + // Only add valid destination + if (destinationLatLongOsm.value.latitude != 0.0 && destinationLatLongOsm.value.longitude != 0.0) { + wayPoints.add(destinationLatLongOsm.value); + } + + if (!isStopMarker) osmMarker.clear(); + + // Add source marker + if (departureLatLongOsm.value.latitude != 0.0 && departureLatLongOsm.value.longitude != 0.0) { + osmMarker.add(flutterMap.Marker(point: departureLatLongOsm.value, width: 40, height: 40, child: departureIconOsm!)); + } + + // Add destination marker + if (destinationLatLongOsm.value.latitude != 0.0 && destinationLatLongOsm.value.longitude != 0.0) { + osmMarker.add(flutterMap.Marker(point: destinationLatLongOsm.value, width: 40, height: 40, child: destinationIconOsm!)); + } + + if (wayPoints.length >= 2) { + await fetchRouteWithWaypoints(wayPoints); + } + } else { + // Google Maps path + fetchGoogleRouteWithWaypoints(); + } + } + + Future fetchGoogleRouteWithWaypoints() async { + if (departureLatLong.value.latitude == 0.0 || destinationLatLong.value.latitude == 0.0) return; + + final origin = '${departureLatLong.value.latitude},${departureLatLong.value.longitude}'; + final destination = '${destinationLatLong.value.latitude},${destinationLatLong.value.longitude}'; + + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/directions/json' + '?origin=$origin&destination=$destination' + '&mode=driving&key=${Constant.mapAPIKey}', + ); + + try { + final response = await http.get(url); + final data = json.decode(response.body); + log("=======>$data"); + if (data['status'] == 'OK') { + final route = data['routes'][0]; + final legs = route['legs'] as List; + + // Polyline + final encodedPolyline = route['overview_polyline']['points']; + final decodedPoints = PolylinePoints.decodePolyline(encodedPolyline); + final coordinates = decodedPoints.map((e) => LatLng(e.latitude, e.longitude)).toList(); + + addPolyLine(coordinates); + + // Distance & Duration + num totalDistance = 0; + num totalDuration = 0; + for (var leg in legs) { + totalDistance += leg['distance']['value']!; // meters + totalDuration += leg['duration']['value']!; // seconds + } + + // Convert distance to KM or Miles + if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { + distance.value = totalDistance / 1000.0; + } else { + distance.value = totalDistance / 1609.34; + } + + // Format duration + final hours = totalDuration ~/ 3600; + final minutes = ((totalDuration % 3600) / 60).round(); + duration.value = '${hours}h ${minutes}m'; + } else { + print('Google Directions API Error: ${data['status']}'); + } + } catch (e) { + print("Google route fetch error: $e"); + } + } + + Future fetchRouteWithWaypoints(List points) async { + final coordinates = points.map((p) => '${p.longitude},${p.latitude}').join(';'); + final url = Uri.parse('https://router.project-osrm.org/route/v1/driving/$coordinates?overview=full&geometries=geojson'); + + try { + final response = await http.get(url); + if (response.statusCode == 200) { + final decoded = json.decode(response.body); + final geometry = decoded['routes'][0]['geometry']['coordinates'] as List; + final dist = decoded['routes'][0]['distance']; + final dur = decoded['routes'][0]['duration']; + + routePoints.clear(); + routePoints.addAll(geometry.map((coord) => latlong.LatLng(coord[1], coord[0]))); + + if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { + distance.value = dist / 1000.00; + } else { + distance.value = dist / 1609.34; + } + + final hours = dur ~/ 3600; + final minutes = ((dur % 3600) / 60).round(); + duration.value = '${hours}h ${minutes}m'; + + // Zoom to fit polyline after drawing + zoomToPolylineOSM(); + } else { + print("Failed to get route: ${response.body}"); + } + } catch (e) { + print("Route fetch error: $e"); + } + } + + void zoomToPolylineOSM() { + if (routePoints.isEmpty) return; + // LatLngBounds requires at least two points + final bounds = flutterMap.LatLngBounds(routePoints.first, routePoints.first); + for (final point in routePoints) { + bounds.extend(point); + } + final center = bounds.center; + // Calculate zoom level to fit all points + double zoom = getBoundsZoomLevel(bounds); + mapOsmController.move(center, zoom); + } + + double getBoundsZoomLevel(flutterMap.LatLngBounds bounds) { + // Simple heuristic: zoom out for larger bounds + final latDiff = (bounds.northEast.latitude - bounds.southWest.latitude).abs(); + final lngDiff = (bounds.northEast.longitude - bounds.southWest.longitude).abs(); + double maxDiff = math.max(latDiff, lngDiff); + if (maxDiff < 0.005) return 18.0; + if (maxDiff < 0.01) return 16.0; + if (maxDiff < 0.05) return 14.0; + if (maxDiff < 0.1) return 12.0; + if (maxDiff < 0.5) return 10.0; + return 8.0; + } + + void addPolyLine(List points) { + final id = const PolylineId("poly"); + final polyline = Polyline(polylineId: id, color: AppThemeData.primary300, points: points, width: 6, geodesic: true); + polyLines[id] = polyline; + + if (points.length >= 2) { + // Zoom to fit all polyline points + updateCameraLocationToFitPolyline(points, mapController); + } + } + + Future updateCameraLocationToFitPolyline(List points, GoogleMapController? mapController) async { + if (mapController == null || points.isEmpty) return; + double minLat = points.first.latitude, maxLat = points.first.latitude; + double minLng = points.first.longitude, maxLng = points.first.longitude; + for (final p in points) { + if (p.latitude < minLat) minLat = p.latitude; + if (p.latitude > maxLat) maxLat = p.latitude; + if (p.longitude < minLng) minLng = p.longitude; + if (p.longitude > maxLng) maxLng = p.longitude; + } + final bounds = LatLngBounds(southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng)); + final cameraUpdate = CameraUpdate.newLatLngBounds(bounds, 50); + await checkCameraLocation(cameraUpdate, mapController); + } + + Future animateToSource(double lat, double long) async { + final hasBothCoords = departureLatLongOsm.value.latitude != 0.0 && destinationLatLongOsm.value.latitude != 0.0; + + if (hasBothCoords) { + await calculateZoomLevel(source: departureLatLongOsm.value, destination: destinationLatLongOsm.value); + } else { + mapOsmController.move(latlong.LatLng(lat, long), 10); + } + } + + RxMap polyLines = {}.obs; + + Future calculateZoomLevel({required latlong.LatLng source, required latlong.LatLng destination, double paddingFraction = 0.001}) async { + final bounds = flutterMap.LatLngBounds.fromPoints([source, destination]); + final screenSize = Size(Get.width, Get.height * 0.5); + const double worldDimension = 256.0; + const double maxZoom = 10.0; + + double latToRad(double lat) => math.log((1 + math.sin(lat * math.pi / 180)) / (1 - math.sin(lat * math.pi / 180))) / 2; + + double computeZoom(double screenPx, double worldPx, double fraction) => math.log(screenPx / worldPx / fraction) / math.ln2; + + final north = bounds.northEast.latitude; + final south = bounds.southWest.latitude; + final east = bounds.northEast.longitude; + final west = bounds.southWest.longitude; + + final latDelta = (north - south).abs(); + final lngDelta = (east - west).abs(); + + final center = bounds.center; + + if (latDelta < 1e-6 || lngDelta < 1e-6) { + mapOsmController.move(center, maxZoom); + } else { + final latFraction = (latToRad(north) - latToRad(south)) / math.pi; + final lngFraction = ((east - west + 360) % 360) / 360; + + final latZoom = computeZoom(screenSize.height, worldDimension, latFraction + paddingFraction); + final lngZoom = computeZoom(screenSize.width, worldDimension, lngFraction + paddingFraction); + + final zoomLevel = math.min(latZoom, lngZoom).clamp(0.0, maxZoom); + mapOsmController.move(center, zoomLevel); + } + } + + Future updateCameraLocation(LatLng source, LatLng destination, GoogleMapController? mapController) async { + if (mapController == null) return; + + final bounds = LatLngBounds( + southwest: LatLng(math.min(source.latitude, destination.latitude), math.min(source.longitude, destination.longitude)), + northeast: LatLng(math.max(source.latitude, destination.latitude), math.max(source.longitude, destination.longitude)), + ); + + final cameraUpdate = CameraUpdate.newLatLngBounds(bounds, 90); + await checkCameraLocation(cameraUpdate, mapController); + } + + Future checkCameraLocation(CameraUpdate cameraUpdate, GoogleMapController mapController) async { + await mapController.animateCamera(cameraUpdate); + final l1 = await mapController.getVisibleRegion(); + final l2 = await mapController.getVisibleRegion(); + + if (l1.southwest.latitude == -90 || l2.southwest.latitude == -90) { + await checkCameraLocation(cameraUpdate, mapController); + } + } + + Future setIcons() async { + try { + if (Constant.selectedMapType == 'osm') { + departureIconOsm = Image.asset("assets/icons/pickup.png", width: 30, height: 30); + destinationIconOsm = Image.asset("assets/icons/dropoff.png", width: 30, height: 30); + taxiIconOsm = Image.asset("assets/icons/ic_taxi.png", width: 30, height: 30); + stopIconOsm = Image.asset("assets/icons/location.png", width: 26, height: 26); + } else { + const config = ImageConfiguration(size: Size(48, 48)); + departureIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/pickup.png"); + destinationIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/dropoff.png"); + taxiIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/ic_taxi.png"); + stopIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/location.png"); + } + } catch (e) { + print('Error loading icons: $e'); + } + } + + void clearMapDataIfLocationsRemoved() { + final isSourceEmpty = departureLatLongOsm.value.latitude == 0.0 && departureLatLongOsm.value.longitude == 0.0; + final isDestinationEmpty = destinationLatLongOsm.value.latitude == 0.0 && destinationLatLongOsm.value.longitude == 0.0; + + if (isSourceEmpty || isDestinationEmpty) { + // Clear polylines + polyLines.clear(); + + // Clear OSM markers (if using OSM) + osmMarker.clear(); + + // Clear Google markers (if using Google Maps) + markers.clear(); + + // Clear route points (optional) + routePoints.clear(); + + // Reset distance and duration values + distance.value = 0.0; + duration.value = ''; + } + } + + void removeSource() { + // Clear departure location and related data + departureLatLongOsm.value = latlong.LatLng(0.0, 0.0); + departureLatLong.value = const LatLng(0.0, 0.0); + sourceTextEditController.value.clear(); + + // Remove marker + if (Constant.selectedMapType == 'osm') { + osmMarker.removeWhere((marker) => marker.point == departureLatLongOsm.value); + } else { + markers.removeWhere((marker) => marker.markerId.value == 'Departure'); + } + + // Clear polylines and route info if needed + clearMapDataIfLocationsRemoved(); + update(); + } + + void removeDestination() { + destinationLatLongOsm.value = latlong.LatLng(0.0, 0.0); + destinationLatLong.value = const LatLng(0.0, 0.0); + destinationTextEditController.value.clear(); + + if (Constant.selectedMapType == 'osm') { + osmMarker.removeWhere((marker) => marker.point == destinationLatLongOsm.value); + } else { + markers.removeWhere((marker) => marker.markerId.value == 'Destination'); + } + + clearMapDataIfLocationsRemoved(); + update(); + } + + Future searchPlaceNameOSM() async { + final url = Uri.parse('https://nominatim.openstreetmap.org/reverse?lat=${departureLatLongOsm.value.latitude}&lon=${departureLatLongOsm.value.longitude}&format=json'); + + final response = await http.get(url, headers: {'User-Agent': 'FlutterMapApp/1.0 (menil.siddhiinfosoft@gmail.com)'}); + + if (response.statusCode == 200) { + log("response.body :: ${response.body}"); + Map data = json.decode(response.body); + sourceTextEditController.value.text = data['display_name'] ?? ''; + } + } + + Future searchPlaceNameGoogle() async { + final lat = departureLatLong.value.latitude; + final lng = departureLatLong.value.longitude; + + final url = Uri.parse('https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lng&key=${Constant.mapAPIKey}'); + + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == 'OK') { + final results = data['results'] as List; + if (results.isNotEmpty) { + final formattedAddress = results[0]['formatted_address']; + sourceTextEditController.value.text = formattedAddress; + } + } else { + log("Google API Error: ${data['status']}"); + } + } else { + log("HTTP Error: ${response.statusCode}"); + } + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'eMart Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": userModel.value.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": userModel.value.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + completeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen(amount: (double.parse(totalAmount) * 100).toString(), currency: "ZAR", secretKey: payStackModel.value.secretKey.toString(), userModel: userModel.value).then(( + value, + ) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": userModel.value.email.toString(), + "phonenumber": userModel.value.phoneNumber, // Add a real phone number + "name": userModel.value.fullName(), // Add a real customer name + }, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: userModel.value).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + // final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + // String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + // + // final response = await http.post( + // Uri.parse(getChecksum), + // headers: {}, + // body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + // ); + // + // final data = jsonDecode(response.body); + // await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + // initiatePayment(amount: amount, orderId: orderId).then((value) { + // String callback = ""; + // if (paytmModel.value.isSandboxEnabled == true) { + // callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } else { + // callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } + // + // GetPaymentTxtTokenModel result = value; + // startTransaction(context, txnTokenBy: result.body.txnToken, orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + // }); + // }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // completeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), "checksum_value": checkSum}, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post( + Uri.parse(initiateURL), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }, + ); + log(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': userModel.value.phoneNumber, 'email': userModel.value.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + // Handle the response + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + // ignore: use_build_context_synchronously + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/address_list_controller.dart b/lib/controllers/address_list_controller.dart new file mode 100644 index 0000000..3a90915 --- /dev/null +++ b/lib/controllers/address_list_controller.dart @@ -0,0 +1,43 @@ +import 'package:customer/models/user_model.dart'; +import 'package:get/get.dart'; + +import '../constant/constant.dart'; +import '../service/fire_store_utils.dart'; + +class AddressListController extends GetxController { + Rx userModel = UserModel().obs; + + RxList shippingAddressList = [].obs; + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getUser(); + super.onInit(); + } + + Future getUser() async { + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) { + if (value != null) { + userModel.value = value; + if (userModel.value.shippingAddress != null) { + shippingAddressList.value = userModel.value.shippingAddress!; + } + } + }); + isLoading.value = false; + } + + Future deleteAddress(int index) async { + if (shippingAddressList.isNotEmpty && index < shippingAddressList.length) { + shippingAddressList.removeAt(index); + userModel.value.shippingAddress = shippingAddressList; + if (shippingAddressList.isNotEmpty) { + Constant.selectedLocation = shippingAddressList.first; + } + await FireStoreUtils.updateUser(userModel.value); + shippingAddressList.refresh(); + } + } +} diff --git a/lib/controllers/advertisement_list_controller.dart b/lib/controllers/advertisement_list_controller.dart new file mode 100644 index 0000000..56ae63b --- /dev/null +++ b/lib/controllers/advertisement_list_controller.dart @@ -0,0 +1,48 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/advertisement_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class AdvertisementListController extends GetxController { + RxBool isLoading = true.obs; + + @override + void onInit() { + getAdvertisementList(); + getFavouriteRestaurant(); + super.onInit(); + } + + RxList advertisementList = [].obs; + + Future getAdvertisementList() async { + advertisementList.clear(); + List allNearestRestaurant = []; + FireStoreUtils.getAllNearestRestaurant().listen((event) async { + allNearestRestaurant.addAll(event); + await FireStoreUtils.getAllAdvertisement().then((value) { + List adsList = value; + advertisementList.addAll( + adsList.where( + (ads) => allNearestRestaurant.any( + (restaurant) => restaurant.id == ads.vendorId, + ), + ), + ); + }); + isLoading.value = false; + }); + } + + RxList favouriteList = [].obs; + + Future getFavouriteRestaurant() async { + if (Constant.userModel != null) { + await FireStoreUtils.getFavouriteRestaurant().then((value) { + favouriteList.value = value; + }); + } + } +} diff --git a/lib/controllers/all_brand_product_controller.dart b/lib/controllers/all_brand_product_controller.dart new file mode 100644 index 0000000..5ffaec1 --- /dev/null +++ b/lib/controllers/all_brand_product_controller.dart @@ -0,0 +1,58 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/brands_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class AllBrandProductController extends GetxController { + RxList productList = [].obs; + Rx brandModel = BrandsModel().obs; + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArguments(); + super.onInit(); + } + + Future getArguments() async { + final arguments = Get.arguments; + if (arguments != null) { + brandModel.value = arguments['brandModel']; + await getProductByCategoryId(); + } + isLoading.value = false; + } + + Future getProductByCategoryId() async { + List productDataList = await FireStoreUtils.getProductListByBrandId(brandModel.value.id.toString()); + + List vendorList = await FireStoreUtils.getAllStoresFuture(); + List allProduct = []; + for (var vendor in vendorList) { + await FireStoreUtils.getAllProducts(vendor.id.toString()).then((value) { + if (Constant.isSubscriptionModelApplied == true || vendor.adminCommission?.isEnabled == true) { + if (vendor.subscriptionPlan != null && Constant.isExpire(vendor) == false) { + if (vendor.subscriptionPlan?.itemLimit == '-1') { + allProduct.addAll(value); + } else { + int selectedProduct = + value.length < int.parse(vendor.subscriptionPlan?.itemLimit ?? '0') ? (value.isEmpty ? 0 : (value.length)) : int.parse(vendor.subscriptionPlan?.itemLimit ?? '0'); + allProduct.addAll(value.sublist(0, selectedProduct)); + } + } + } else { + allProduct.addAll(value); + } + }); + } + for (var element in productDataList) { + final bool productIsInList = allProduct.any((product) => product.id == element.id); + if (productIsInList) { + productList.add(element); + } + } + } +} diff --git a/lib/controllers/all_category_product_controller.dart b/lib/controllers/all_category_product_controller.dart new file mode 100644 index 0000000..d5454f6 --- /dev/null +++ b/lib/controllers/all_category_product_controller.dart @@ -0,0 +1,30 @@ +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class AllCategoryProductController extends GetxController { + RxBool isLoading = true.obs; + Rx categoryModel = VendorCategoryModel().obs; + RxList productList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getArguments(); + super.onInit(); + } + + Future getArguments() async { + final arguments = Get.arguments; + if (arguments != null) { + categoryModel.value = arguments['categoryModel']; + await getProductByCategoryId(); + } + isLoading.value = false; + } + + Future getProductByCategoryId() async { + productList.value = await FireStoreUtils.getProductListByCategoryId(categoryModel.value.id.toString()); + } +} diff --git a/lib/controllers/book_parcel_controller.dart b/lib/controllers/book_parcel_controller.dart new file mode 100644 index 0000000..7edc29d --- /dev/null +++ b/lib/controllers/book_parcel_controller.dart @@ -0,0 +1,340 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/widget/geoflutterfire/src/geoflutterfire.dart'; +import 'package:dropdown_textfield/dropdown_textfield.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as latlong; +import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; +import '../constant/constant.dart'; +import '../models/parcel_category.dart'; +import '../models/parcel_order_model.dart'; +import '../models/parcel_weight_model.dart'; +import '../models/user_model.dart'; +import '../screen_ui/parcel_service/parcel_order_confirmation.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; + +class BookParcelController extends GetxController { + // Sender details + final Rx senderLocationController = TextEditingController().obs; + final Rx senderNameController = TextEditingController().obs; + final Rx senderMobileController = TextEditingController().obs; + final Rx senderWeightController = SingleValueDropDownController().obs; + final Rx senderNoteController = TextEditingController().obs; + final Rx senderCountryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs; + + // Receiver details + final Rx receiverLocationController = TextEditingController().obs; + final Rx receiverNameController = TextEditingController().obs; + final Rx receiverMobileController = TextEditingController().obs; + final Rx receiverNoteController = TextEditingController().obs; + final Rx receiverCountryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs; + + // Delivery type + final RxString selectedDeliveryType = 'now'.obs; + + // Scheduled delivery fields + final Rx scheduledDateController = TextEditingController().obs; + final Rx scheduledTimeController = TextEditingController().obs; + final RxString scheduledDate = ''.obs; + final RxString scheduledTime = ''.obs; + + // Parcel weight list + final RxList parcelWeight = [].obs; + + final RxList images = [].obs; + final ImagePicker _picker = ImagePicker(); + + Rx senderLocation = Rx(null); + Rx receiverLocation = Rx(null); + + ParcelWeightModel? selectedWeight; + ParcelCategory? selectedCategory; + + // UI observables + RxBool isScheduled = false.obs; + RxDouble distance = 0.0.obs; + RxDouble duration = 0.0.obs; + RxDouble subTotal = 0.0.obs; + + @override + void onInit() { + super.onInit(); + setArguments(); + getParcelWeight(); + setCurrentLocationForSenderAndReceiver(); + } + + void setArguments() { + if (Get.arguments != null && Get.arguments['parcelCategory'] != null) { + selectedCategory = Get.arguments['parcelCategory']; + } + } + + Future getParcelWeight() async { + parcelWeight.value = await FireStoreUtils.getParcelWeight(); + } + + Future pickScheduledDate(BuildContext context) async { + final DateTime? picked = await showDatePicker(context: context, initialDate: DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365))); + if (picked != null) { + final formattedDate = "${picked.day}/${picked.month}/${picked.year}"; + scheduledDate.value = formattedDate; + scheduledDateController.value.text = formattedDate; + } + } + + Future pickScheduledTime(BuildContext context) async { + final TimeOfDay? picked = await showTimePicker(context: context, initialTime: TimeOfDay.now()); + if (picked != null) { + final formattedTime = picked.format(context); + scheduledTime.value = formattedTime; + scheduledTimeController.value.text = formattedTime; + } + } + + void onCameraClick(BuildContext context) { + final action = CupertinoActionSheet( + message: Text('Add your parcel image.'.tr, style: const TextStyle(fontSize: 15.0)), + actions: [ + CupertinoActionSheetAction( + child: Text('Choose image from gallery'.tr), + onPressed: () async { + Navigator.pop(context); + final imageList = await _picker.pickMultiImage(); + if (imageList.isNotEmpty) { + images.addAll(imageList); + } + }, + ), + CupertinoActionSheetAction( + child: Text('Take a picture'.tr), + onPressed: () async { + Navigator.pop(context); + final XFile? photo = await _picker.pickImage(source: ImageSource.camera); + if (photo != null) { + images.add(photo); + } + }, + ), + ], + cancelButton: CupertinoActionSheetAction(child: Text('Cancel'.tr), onPressed: () => Navigator.pop(context)), + ); + showCupertinoModalPopup(context: context, builder: (context) => action); + } + + Future setCurrentLocationForSenderAndReceiver() async { + try { + await Geolocator.requestPermission(); + final position = await Geolocator.getCurrentPosition(); + final placemarks = await placemarkFromCoordinates(position.latitude, position.longitude); + final place = placemarks.first; + final address = "${place.name}, ${place.subLocality}, ${place.locality}, ${place.administrativeArea}, ${place.postalCode}, ${place.country}"; + + final userLocation = UserLocation(latitude: position.latitude, longitude: position.longitude); + senderLocation.value = userLocation; + senderLocationController.value.text = address; + } catch (e) { + debugPrint("Failed to fetch current location: $e"); + } + } + + bool validateFields() { + if (senderNameController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter sender name".tr); + return false; + } else if (senderMobileController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter sender mobile".tr); + return false; + } else if (senderLocationController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter sender address".tr); + return false; + } else if (receiverNameController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter receiver name".tr); + return false; + } else if (receiverMobileController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter receiver mobile".tr); + return false; + } else if (receiverLocationController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter receiver address".tr); + return false; + } else if (isScheduled.value) { + if (scheduledDate.value.isEmpty) { + ShowToastDialog.showToast("Please select scheduled date".tr); + return false; + } else if (scheduledTime.value.isEmpty) { + ShowToastDialog.showToast("Please select scheduled time".tr); + return false; + } + } + + if (selectedWeight == null) { + ShowToastDialog.showToast("Please select parcel weight".tr); + return false; + } else if (senderLocation.value == null || receiverLocation.value == null) { + ShowToastDialog.showToast("Please select both sender and receiver locations".tr); + return false; + } + return true; + } + + Future bookNow() async { + if (!validateFields()) return; + + try { + distance.value = 0.0; + + if (Constant.selectedMapType == 'osm') { + print("Fetching route using OSM"); + print("Sender Location: ${senderLocation.value?.latitude}, ${senderLocation.value?.longitude}"); + print("Receiver Location: ${receiverLocation.value?.latitude}, ${receiverLocation.value?.longitude}"); + await fetchRouteWithWaypoints([ + latlong.LatLng(senderLocation.value?.latitude ?? 0.0, senderLocation.value?.longitude ?? 0.0), + latlong.LatLng(receiverLocation.value?.latitude ?? 0.0, receiverLocation.value?.longitude ?? 0.0), + ]); + } else { + await fetchGoogleRouteWithWaypoints(); + } + + if (distance.value < 0.5) { + ShowToastDialog.showToast("Sender's location to receiver's location should be more than 1 km.".tr); + return; + } + + subTotal.value = (distance.value * double.parse(selectedWeight!.deliveryCharge.toString())); + goToCart(); + } catch (e) { + ShowToastDialog.showToast("Something went wrong while booking.".tr); + debugPrint("bookNow error: $e"); + } + } + + void goToCart() { + DateTime senderPickup = isScheduled.value ? parseScheduledDateTime(scheduledDate.value, scheduledTime.value) : DateTime.now(); + + print("Sender Pickup: $distance"); + ParcelOrderModel order = ParcelOrderModel( + id: Constant.getUuid(), + subTotal: subTotal.value.toString(), + parcelType: selectedCategory?.title ?? '', + parcelCategoryID: selectedCategory?.id ?? '', + note: senderNoteController.value.text, + receiverNote: receiverNoteController.value.text, + distance: distance.value.toStringAsFixed(4), + parcelWeight: selectedWeight?.title ?? '', + parcelWeightCharge: selectedWeight?.deliveryCharge, + sendToDriver: isScheduled.value == true ? false : true, + senderPickupDateTime: Timestamp.fromDate(senderPickup), + receiverPickupDateTime: Timestamp.fromDate(DateTime.now()), + taxSetting: Constant.taxList, + isSchedule: isScheduled.value, + sourcePoint: G( + geopoint: GeoPoint(senderLocation.value!.latitude ?? 0.0, senderLocation.value!.longitude ?? 0.0), + geohash: Geoflutterfire().point(latitude: senderLocation.value!.latitude ?? 0.0, longitude: senderLocation.value!.longitude ?? 0.0).hash, + ), + destinationPoint: G( + geopoint: GeoPoint(receiverLocation.value!.latitude ?? 0.0, receiverLocation.value!.longitude ?? 0.0), + geohash: Geoflutterfire().point(latitude: receiverLocation.value!.latitude ?? 0.0, longitude: receiverLocation.value!.longitude ?? 0.0).hash, + ), + sender: LocationInformation( + address: senderLocationController.value.text, + name: senderNameController.value.text, + phone: "(${senderCountryCodeController.value.text}) ${senderMobileController.value.text}", + ), + receiver: LocationInformation( + address: receiverLocationController.value.text, + name: receiverNameController.value.text, + phone: "(${receiverCountryCodeController.value.text}) ${receiverMobileController.value.text}", + ), + receiverLatLong: receiverLocation.value, + senderLatLong: senderLocation.value, + sectionId: Constant.sectionConstantModel?.id ?? '', + ); + + debugPrint("Order Distance: ${distance.value}"); + debugPrint("Subtotal: ${subTotal.value}"); + debugPrint("Order JSON: ${order.toJson()}"); + + Get.to(() => ParcelOrderConfirmationScreen(), arguments: {'parcelOrder': order, 'images': images}); + } + + DateTime parseScheduledDateTime(String dateStr, String timeStr) { + try { + final dateParts = dateStr.split('/'); + final day = int.parse(dateParts[0]); + final month = int.parse(dateParts[1]); + final year = int.parse(dateParts[2]); + + final time = TimeOfDay(hour: int.parse(timeStr.split(':')[0]), minute: int.parse(timeStr.split(':')[1].split(' ')[0])); + final isPM = timeStr.toLowerCase().contains('pm'); + final hour24 = isPM && time.hour < 12 ? time.hour + 12 : time.hour; + + return DateTime(year, month, day, hour24, time.minute); + } catch (e) { + debugPrint("Failed to parse scheduled date/time: $e"); + return DateTime.now(); + } + } + + Future fetchGoogleRouteWithWaypoints() async { + final origin = '${senderLocation.value!.latitude},${senderLocation.value!.longitude}'; + final destination = '${receiverLocation.value!.latitude},${receiverLocation.value!.longitude}'; + final url = Uri.parse('https://maps.googleapis.com/maps/api/directions/json?origin=$origin&destination=$destination&mode=driving&key=${Constant.mapAPIKey}'); + + try { + final response = await http.get(url); + final data = json.decode(response.body); + if (data['status'] == 'OK') { + final route = data['routes'][0]; + final legs = route['legs'] as List; + num totalDistance = 0; + num totalDuration = 0; + for (var leg in legs) { + totalDistance += leg['distance']['value']; + totalDuration += leg['duration']['value']; + } + if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { + distance.value = totalDistance / 1000.0; + } else { + distance.value = totalDistance / 1609.34; + } + duration.value = (totalDuration / 60).round().toDouble(); + } else { + debugPrint('Google Directions API Error: ${data['status']}'); + } + } catch (e) { + debugPrint("Google route fetch error: $e"); + } + } + + Future fetchRouteWithWaypoints(List points) async { + final coordinates = points.map((p) => '${p.longitude},${p.latitude}').join(';'); + final url = Uri.parse('https://router.project-osrm.org/route/v1/driving/$coordinates?overview=full&geometries=geojson'); + + try { + final response = await http.get(url); + if (response.statusCode == 200) { + final decoded = json.decode(response.body); + final dist = decoded['routes'][0]['distance']; + final dur = decoded['routes'][0]['duration']; + + if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { + distance.value = dist / 1000.00; + } else { + distance.value = dist / 1609.34; + } + duration.value = (dur / 60).round().toDouble(); + } else { + debugPrint("Failed to get route: ${response.body}"); + } + } catch (e) { + debugPrint("Route fetch error: $e"); + } + } +} diff --git a/lib/controllers/cab_booking_controller.dart b/lib/controllers/cab_booking_controller.dart new file mode 100644 index 0000000..9b80a10 --- /dev/null +++ b/lib/controllers/cab_booking_controller.dart @@ -0,0 +1,1596 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:math' as maths; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/collection_name.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cab_order_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/payment_model/cod_setting_model.dart'; +import 'package:customer/models/payment_model/flutter_wave_model.dart'; +import 'package:customer/models/payment_model/mercado_pago_model.dart'; +import 'package:customer/models/payment_model/mid_trans.dart'; +import 'package:customer/models/payment_model/orange_money.dart'; +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:customer/models/payment_model/pay_stack_model.dart'; +import 'package:customer/models/payment_model/paypal_model.dart'; +import 'package:customer/models/payment_model/paytm_model.dart'; +import 'package:customer/models/payment_model/razorpay_model.dart'; +import 'package:customer/models/payment_model/stripe_model.dart'; +import 'package:customer/models/payment_model/wallet_setting_model.dart'; +import 'package:customer/models/payment_model/xendit.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vehicle_type.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/payment/MercadoPagoScreen.dart'; +import 'package:customer/payment/PayFastScreen.dart'; +import 'package:customer/payment/getPaytmTxtToken.dart'; +import 'package:customer/payment/midtrans_screen.dart'; +import 'package:customer/payment/orangePayScreen.dart'; +import 'package:customer/payment/paystack/pay_stack_screen.dart'; +import 'package:customer/payment/paystack/pay_stack_url_model.dart'; +import 'package:customer/payment/paystack/paystack_url_genrater.dart'; +import 'package:customer/payment/stripe_failed_model.dart'; +import 'package:customer/payment/xenditModel.dart'; +import 'package:customer/payment/xenditScreen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:http/http.dart' as http; +import 'package:latlong2/latlong.dart' as latlong; +import 'package:location/location.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../themes/app_them_data.dart'; + +class CabBookingController extends GetxController { + late GoogleMapController mapController; + final flutterMap.MapController mapOsmController = flutterMap.MapController(); + + final Rx sourceTextEditController = TextEditingController().obs; + final Rx destinationTextEditController = TextEditingController().obs; + + final Rx couponCodeTextEditController = TextEditingController().obs; + + final Rx currentLocation = Location().obs; + + final RxSet markers = {}.obs; + final RxList osmMarker = [].obs; + final RxList routePoints = [].obs; + + final Rx currentPosition = LatLng(23.0225, 72.5714).obs; + + final Rx departureLatLong = const LatLng(0.0, 0.0).obs; + final Rx destinationLatLong = const LatLng(0.0, 0.0).obs; + final Rx departureLatLongOsm = latlong.LatLng(0.0, 0.0).obs; + final Rx destinationLatLongOsm = latlong.LatLng(0.0, 0.0).obs; + + final RxBool isLoading = true.obs; + + final RxDouble distance = 0.0.obs; + final RxString duration = ''.obs; + + BitmapDescriptor? departureIcon, destinationIcon, taxiIcon, stopIcon; + Widget? departureIconOsm, destinationIconOsm, taxiIconOsm, stopIconOsm; + + RxList taxList = [].obs; + RxList vehicleTypes = [].obs; + Rx selectedVehicleType = VehicleType().obs; + + Rx userModel = UserModel().obs; + Rx driverModel = UserModel().obs; + Rx currentOrder = CabOrderModel().obs; + + final RxString selectedPaymentMethod = ''.obs; + final RxString bottomSheetType = 'location'.obs; + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + bool isOsmMapReady = false; + + Rx selectedCouponModel = CouponModel().obs; + + @override + void onInit() { + super.onInit(); + taxList.value = Constant.taxList; + initData(); + } + + Future initData() async { + if (Constant.selectedMapType == 'osm') { + mapOsmController; + } + await setIcons(); + await getVehicleType(); + isLoading.value = false; + } + + RxList cabCouponList = [].obs; + + Future getVehicleType() async { + final vehicleList = await FireStoreUtils.getVehicleType(); + vehicleTypes.value = vehicleList; + if (vehicleTypes.isNotEmpty) { + selectedVehicleType.value = vehicleTypes.first; + } + + await getPaymentSettings(); + + FireStoreUtils.fireStore.collection(CollectionName.users).doc(FireStoreUtils.getCurrentUid()).snapshots().listen((userSnapshot) async { + if (!userSnapshot.exists) return; + + userModel.value = UserModel.fromJson(userSnapshot.data()!); + + if (userModel.value.inProgressOrderID != null && userModel.value.inProgressOrderID!.isNotEmpty) { + String? validRideId; + + for (String id in userModel.value.inProgressOrderID!) { + final rideDoc = await FireStoreUtils.fireStore.collection(CollectionName.rides).doc(id).get(); + + if (rideDoc.exists && (rideDoc.data()?['rideType'] ?? '').toString().toLowerCase() == "ride") { + validRideId = userModel.value.inProgressOrderID!.first!; + break; + } + } + + FireStoreUtils.fireStore.collection(CollectionName.rides).doc(validRideId).snapshots().listen((rideSnapshot) async { + if (!rideSnapshot.exists) return; + + final rideData = rideSnapshot.data()!; + currentOrder.value = CabOrderModel.fromJson(rideData); + final status = currentOrder.value.status; + + if (status == Constant.driverAccepted || status == Constant.orderInTransit) { + FireStoreUtils.fireStore.collection(CollectionName.users).doc(currentOrder.value.driverId).snapshots().listen((event) async { + if (event.exists && event.data() != null) { + UserModel driverModel0 = UserModel.fromJson(event.data()!); + driverModel.value = driverModel0; + await updateDriverRoute(driverModel0); + } + }); + } + + print("Current Ride Status: $status"); + if (status == Constant.orderPlaced || status == Constant.driverPending || status == Constant.driverRejected || (status == Constant.orderAccepted && currentOrder.value.driverId == null)) { + bottomSheetType.value = 'waitingForDriver'; + } else if (status == Constant.driverAccepted || status == Constant.orderInTransit) { + bottomSheetType.value = 'driverDetails'; + sourceTextEditController.value.text = currentOrder.value.sourceLocationName ?? ''; + destinationTextEditController.value.text = currentOrder.value.destinationLocationName ?? ''; + selectedPaymentMethod.value = currentOrder.value.paymentMethod ?? ''; + calculateTotalAmountAfterAccept(); + } else if (status == Constant.orderCompleted) { + userModel.value.inProgressOrderID!.remove(validRideId); + await FireStoreUtils.updateUser(userModel.value); + bottomSheetType.value = 'location'; + Get.back(); + } + }); + } else { + bottomSheetType.value = 'location'; + if (Constant.currentLocation != null) { + setDepartureMarker(Constant.currentLocation!.latitude, Constant.currentLocation!.longitude); + searchPlaceNameOSM(); + } + } + }); + + final coupons = await FireStoreUtils.getCabCoupon(); + cabCouponList.value = coupons; + } + + Future updateDriverRoute(UserModel driverModel) async { + try { + final order = currentOrder.value; + + final driverLat = driverModel.location!.latitude ?? 0.0; + final driverLng = driverModel.location!.longitude ?? 0.0; + + if (driverLat == 0.0 || driverLng == 0.0) return; + + // Get pickup and destination + final pickupLat = order.sourceLocation?.latitude ?? 0.0; + final pickupLng = order.sourceLocation?.longitude ?? 0.0; + final destLat = order.destinationLocation?.latitude ?? 0.0; + final destLng = order.destinationLocation?.longitude ?? 0.0; + + if (Constant.selectedMapType == 'osm') { + /// For OpenStreetMap + routePoints.clear(); + + if (order.status == Constant.driverAccepted) { + // DRIVER → PICKUP + await fetchRouteWithWaypoints([latlong.LatLng(driverLat, driverLng), latlong.LatLng(pickupLat, pickupLng)]); + } else if (order.status == Constant.orderInTransit) { + // PICKUP → DESTINATION + await fetchRouteWithWaypoints([latlong.LatLng(driverLat, driverLng), latlong.LatLng(destLat, destLng)]); + } + updateRouteMarkers(driverModel); + } else { + /// For Google Maps + if (order.status == Constant.driverAccepted) { + await fetchGoogleRouteBetween(LatLng(driverLat, driverLng), LatLng(pickupLat, pickupLng)); + } else if (order.status == Constant.orderInTransit) { + await fetchGoogleRouteBetween(LatLng(driverLat, driverLng), LatLng(destLat, destLng)); + } + updateRouteMarkers(driverModel); + } + } catch (e) { + print("Error in updateDriverRoute: $e"); + } + } + + Future updateRouteMarkers(UserModel driverModel) async { + try { + final order = currentOrder.value; + if (order.driver == null || driverModel.location == null) return; + + final driverLat = driverModel.location!.latitude ?? 0.0; + final driverLng = driverModel.location!.longitude ?? 0.0; + final pickupLat = order.sourceLocation?.latitude ?? 0.0; + final pickupLng = order.sourceLocation?.longitude ?? 0.0; + final destLat = order.destinationLocation?.latitude ?? 0.0; + final destLng = order.destinationLocation?.longitude ?? 0.0; + + markers.clear(); + osmMarker.clear(); + + final departureBytes = await Constant().getBytesFromAsset('assets/images/location_black3x.png', 50); + final destinationBytes = await Constant().getBytesFromAsset('assets/images/location_orange3x.png', 50); + final driverBytesRaw = + (Constant.sectionConstantModel?.markerIcon?.isNotEmpty ?? false) + ? await Constant().getBytesFromUrl(Constant.sectionConstantModel!.markerIcon!, width: 120) + : await Constant().getBytesFromAsset('assets/images/ic_cab.png', 50); + + departureIcon = BitmapDescriptor.fromBytes(departureBytes); + destinationIcon = BitmapDescriptor.fromBytes(destinationBytes); + taxiIcon = BitmapDescriptor.fromBytes(driverBytesRaw); + + if (Constant.selectedMapType == 'osm') { + if (order.status == Constant.driverAccepted) { + osmMarker.addAll([ + flutterMap.Marker(point: latlong.LatLng(pickupLat, pickupLng), width: 40, height: 40, child: Image.asset('assets/images/location_black3x.png', width: 40)), + flutterMap.Marker( + point: latlong.LatLng(driverLat, driverLng), + width: 45, + height: 45, + rotate: true, + child: CachedNetworkImage( + width: 50, + height: 50, + imageUrl: Constant.sectionConstantModel!.markerIcon.toString(), + placeholder: (context, url) => Constant.loader(), + errorWidget: (context, url, error) => SizedBox(width: 30, height: 30, child: CircularProgressIndicator(strokeWidth: 2)), + ), + ), + ]); + } else if (order.status == Constant.orderInTransit) { + osmMarker.addAll([ + flutterMap.Marker(point: latlong.LatLng(destLat, destLng), width: 40, height: 40, child: Image.asset('assets/images/location_orange3x.png', width: 40)), + flutterMap.Marker( + point: latlong.LatLng(driverLat, driverLng), + width: 45, + height: 45, + rotate: true, + child: CachedNetworkImage( + width: 50, + height: 50, + imageUrl: Constant.sectionConstantModel!.markerIcon.toString(), + placeholder: (context, url) => Constant.loader(), + errorWidget: (context, url, error) => SizedBox(width: 30, height: 30, child: CircularProgressIndicator(strokeWidth: 2)), + ), + ), + ]); + } + } else { + if (order.status == Constant.driverAccepted) { + markers.addAll([ + Marker( + markerId: const MarkerId("pickup"), + position: LatLng(pickupLat, pickupLng), + infoWindow: InfoWindow(title: "Pickup Location".tr), + icon: departureIcon ?? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), + ), + Marker(markerId: const MarkerId("driver"), position: LatLng(driverLat, driverLng), infoWindow: InfoWindow(title: "Driver at Pickup".tr), icon: taxiIcon ?? BitmapDescriptor.defaultMarker), + ]); + } else if (order.status == Constant.orderInTransit) { + markers.addAll([ + Marker( + markerId: const MarkerId("destination"), + position: LatLng(destLat, destLng), + infoWindow: InfoWindow(title: "Destination Location".tr), + icon: destinationIcon ?? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), + ), + Marker(markerId: const MarkerId("driver"), position: LatLng(driverLat, driverLng), infoWindow: InfoWindow(title: "Driver Location".tr), icon: taxiIcon ?? BitmapDescriptor.defaultMarker), + ]); + } + } + + update(); + } catch (e) { + print("❌ Error in updateRouteMarkers: $e"); + } + } + + Future fetchGoogleRouteBetween(LatLng originPoint, LatLng destPoint) async { + final origin = '${originPoint.latitude},${originPoint.longitude}'; + final destination = '${destPoint.latitude},${destPoint.longitude}'; + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/directions/json' + '?origin=$origin&destination=$destination' + '&mode=driving&key=${Constant.mapAPIKey}', + ); + + try { + final response = await http.get(url); + final data = json.decode(response.body); + + if (data['status'] == 'OK') { + final route = data['routes'][0]; + final encodedPolyline = route['overview_polyline']['points']; + final decodedPoints = PolylinePoints.decodePolyline(encodedPolyline); + final coordinates = decodedPoints.map((e) => LatLng(e.latitude, e.longitude)).toList(); + + addPolyLine(coordinates); + + // Distance + duration update + final leg = route['legs'][0]; + final totalDistance = leg['distance']['value'] / 1000.0; + final totalDuration = leg['duration']['value'] / 60.0; + + distance.value = totalDistance; + duration.value = '${totalDuration.toStringAsFixed(0)} min'; + } else { + print('Google Directions API error: ${data['status']}'); + } + } catch (e) { + print("Error fetching driver route: $e"); + } + } + + void calculateTotalAmountAfterAccept() { + taxAmount = 0.0.obs; + discount = 0.0.obs; + subTotal.value = double.parse(currentOrder.value.subTotal.toString()); + discount.value = double.parse(currentOrder.value.discount ?? '0.0'); + + if (currentOrder.value.taxSetting != null) { + for (var element in currentOrder.value.taxSetting!) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + update(); + } + + void calculateTotalAmount() { + subTotal = 0.0.obs; + taxAmount = 0.0.obs; + discount = 0.0.obs; + totalAmount = 0.0.obs; + subTotal.value = getAmount(selectedVehicleType.value); + + if (selectedCouponModel.value.id != null) { + discount.value = Constant.calculateDiscount(amount: subTotal.value.toString(), offerModel: selectedCouponModel.value); + } + + for (var element in Constant.taxList) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + update(); + } + + Future completeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.cod.name) { + currentOrder.value.paymentMethod = selectedPaymentMethod.value; + await FireStoreUtils.cabOrderPlace(currentOrder.value).then((value) { + ShowToastDialog.showToast("Payment method changed".tr); + Get.back(); + Get.back(); + }); + } else { + currentOrder.value.paymentStatus = true; + currentOrder.value.paymentMethod = selectedPaymentMethod.value; + userModel.value.inProgressOrderID ??= []; + userModel.value.inProgressOrderID!.clear(); + await FireStoreUtils.updateUser(userModel.value); + + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: currentOrder.value.id, + note: "Cab Amount debited", + paymentStatus: "success", + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()); + } + }); + } + + await FireStoreUtils.cabOrderPlace(currentOrder.value).then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + Get.back(); + }); + } + } + + Future placeOrder() async { + DestinationLocation sourceLocation = DestinationLocation( + latitude: Constant.selectedMapType == 'osm' ? departureLatLongOsm.value.latitude : departureLatLong.value.latitude, + longitude: Constant.selectedMapType == 'osm' ? departureLatLongOsm.value.longitude : departureLatLong.value.longitude, + ); + + DestinationLocation destinationLocation = DestinationLocation( + latitude: Constant.selectedMapType == 'osm' ? destinationLatLongOsm.value.latitude : destinationLatLong.value.latitude, + longitude: Constant.selectedMapType == 'osm' ? destinationLatLongOsm.value.longitude : destinationLatLong.value.longitude, + ); + + CabOrderModel orderModel = CabOrderModel(); + orderModel.id = const Uuid().v4(); + orderModel.distance = distance.value.toString(); + orderModel.duration = duration.value; + orderModel.vehicleId = selectedVehicleType.value.id; + orderModel.vehicleType = selectedVehicleType.value; + orderModel.authorID = FireStoreUtils.getCurrentUid(); + orderModel.sourceLocationName = sourceTextEditController.value.text; + orderModel.destinationLocationName = destinationTextEditController.value.text; + + orderModel.sourceLocation = sourceLocation; + orderModel.destinationLocation = destinationLocation; + orderModel.author = userModel.value; + orderModel.subTotal = subTotal.value.toString(); + orderModel.discount = discount.value.toString(); + orderModel.couponCode = selectedCouponModel.value.code; + orderModel.couponId = selectedCouponModel.value.id; + + orderModel.taxSetting = Constant.taxList; + orderModel.adminCommissionType = + Constant.sectionConstantModel!.adminCommision != null && Constant.sectionConstantModel!.adminCommision!.isEnabled == true + ? Constant.sectionConstantModel!.adminCommision!.commissionType.toString() + : null; + orderModel.adminCommission = + Constant.sectionConstantModel!.adminCommision != null && Constant.sectionConstantModel!.adminCommision!.isEnabled == true + ? Constant.sectionConstantModel!.adminCommision!.amount.toString() + : null; + orderModel.couponCode = couponCodeTextEditController.value.text; + orderModel.paymentMethod = selectedPaymentMethod.value; + orderModel.paymentStatus = false; + orderModel.triggerDelevery = Timestamp.now(); + orderModel.tipAmount = "0.0"; + orderModel.scheduleReturnDateTime = Timestamp.now(); + orderModel.rideType = 'ride'; + orderModel.roundTrip = false; + orderModel.sectionId = Constant.sectionConstantModel!.id; + orderModel.createdAt = Timestamp.now(); + orderModel.otpCode = (maths.Random().nextInt(9000) + 1000).toString(); // Generate a 4-digit OTP + orderModel.status = Constant.orderPlaced; + orderModel.scheduleDateTime = Timestamp.now(); + log("Order Model : ${orderModel.toJson()}"); + + await FireStoreUtils.cabOrderPlace(orderModel); + + userModel.value.inProgressOrderID!.add(orderModel.id); + await FireStoreUtils.updateUser(userModel.value); + + bottomSheetType.value = 'waitingForDriver'; + } + + double getAmount(VehicleType vehicleType) { + final double currentDistance = distance.value; + if (currentDistance <= (vehicleType.minimum_delivery_charges_within_km ?? 0)) { + return double.tryParse(vehicleType.minimum_delivery_charges.toString()) ?? 0.0; + } else { + return (vehicleType.delivery_charges_per_km ?? 0.0) * currentDistance; + } + } + + void setDepartureMarker(double lat, double long) { + if (Constant.selectedMapType == 'osm') { + _setOsmMarker(lat, long, isDeparture: true); + } else { + _setGoogleMarker(lat, long, isDeparture: true); + } + } + + void setDestinationMarker(double lat, double lng) { + if (Constant.selectedMapType == 'osm') { + _setOsmMarker(lat, lng, isDeparture: false); + } else { + _setGoogleMarker(lat, lng, isDeparture: false); + } + } + + void setStopMarker(double lat, double lng, int index) { + if (Constant.selectedMapType == 'osm') { + // Add new stop marker without clearing + osmMarker.add(flutterMap.Marker(point: latlong.LatLng(lat, lng), width: 40, height: 40, child: stopIconOsm!)); + + getDirections(isStopMarker: true); + } else { + final markerId = MarkerId('Stop $index'); + + markers.removeWhere((marker) => marker.markerId == markerId); + markers.add(Marker(markerId: markerId, infoWindow: InfoWindow(title: '${'Stop'.tr} ${String.fromCharCode(index + 65)}'), position: LatLng(lat, lng), icon: stopIcon!)); + + getDirections(); + } + } + + void _setOsmMarker(double lat, double lng, {required bool isDeparture}) { + final marker = flutterMap.Marker(point: latlong.LatLng(lat, lng), width: 40, height: 40, child: isDeparture ? departureIconOsm! : destinationIconOsm!); + if (isDeparture) { + departureLatLongOsm.value = latlong.LatLng(lat, lng); + } else { + destinationLatLongOsm.value = latlong.LatLng(lat, lng); + } + osmMarker.add(marker); + if (departureLatLongOsm.value.latitude != 0 && destinationLatLongOsm.value.latitude != 0) { + getDirections(); + animateToSource(lat, lng); + } + } + + void _setGoogleMarker(double lat, double lng, {required bool isDeparture}) { + final LatLng pos = LatLng(lat, lng); + final markerId = MarkerId(isDeparture ? 'Departure' : 'Destination'); + final icon = isDeparture ? departureIcon! : destinationIcon!; + final title = isDeparture ? 'Departure'.tr : 'Destination'.tr; + + if (isDeparture) { + departureLatLong.value = pos; + } else { + destinationLatLong.value = pos; + } + + // Remove only the matching departure/destination marker + markers.removeWhere((marker) => marker.markerId == markerId); + + // Add new marker + markers.add(Marker(markerId: markerId, position: pos, icon: icon, infoWindow: InfoWindow(title: title))); + + mapController.animateCamera(CameraUpdate.newLatLngZoom(pos, 14)); + + if (departureLatLong.value.latitude != 0 && destinationLatLong.value.latitude != 0) { + getDirections(); + } else { + mapController.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(target: LatLng(lat, lng), zoom: 14))); + } + } + + Future getDirections({bool isStopMarker = false}) async { + if (Constant.selectedMapType == 'osm') { + final wayPoints = []; + + // Only add valid source + if (departureLatLongOsm.value.latitude != 0.0 && departureLatLongOsm.value.longitude != 0.0) { + wayPoints.add(departureLatLongOsm.value); + } + + // Only add valid destination + if (destinationLatLongOsm.value.latitude != 0.0 && destinationLatLongOsm.value.longitude != 0.0) { + wayPoints.add(destinationLatLongOsm.value); + } + + if (!isStopMarker) osmMarker.clear(); + + // Add source marker + if (departureLatLongOsm.value.latitude != 0.0 && departureLatLongOsm.value.longitude != 0.0) { + osmMarker.add(flutterMap.Marker(point: departureLatLongOsm.value, width: 40, height: 40, child: departureIconOsm!)); + } + + // Add destination marker + if (destinationLatLongOsm.value.latitude != 0.0 && destinationLatLongOsm.value.longitude != 0.0) { + osmMarker.add(flutterMap.Marker(point: destinationLatLongOsm.value, width: 40, height: 40, child: destinationIconOsm!)); + } + + if (wayPoints.length >= 2) { + await fetchRouteWithWaypoints(wayPoints); + } + } else { + // Google Maps path + fetchGoogleRouteWithWaypoints(); + } + } + + Future fetchGoogleRouteWithWaypoints() async { + if (departureLatLong.value.latitude == 0.0 || destinationLatLong.value.latitude == 0.0) return; + + final origin = '${departureLatLong.value.latitude},${departureLatLong.value.longitude}'; + final destination = '${destinationLatLong.value.latitude},${destinationLatLong.value.longitude}'; + + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/directions/json' + '?origin=$origin&destination=$destination' + '&mode=driving&key=${Constant.mapAPIKey}', + ); + + try { + final response = await http.get(url); + final data = json.decode(response.body); + log("=======>$data"); + if (data['status'] == 'OK') { + final route = data['routes'][0]; + final legs = route['legs'] as List; + + // Polyline + final encodedPolyline = route['overview_polyline']['points']; + final decodedPoints = PolylinePoints.decodePolyline(encodedPolyline); + final coordinates = decodedPoints.map((e) => LatLng(e.latitude, e.longitude)).toList(); + + addPolyLine(coordinates); + + // Distance & Duration + num totalDistance = 0; + num totalDuration = 0; + for (var leg in legs) { + totalDistance += leg['distance']['value']!; // meters + totalDuration += leg['duration']['value']!; // seconds + } + + // Convert distance to KM or Miles + if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { + distance.value = totalDistance / 1000.0; + } else { + distance.value = totalDistance / 1609.34; + } + + // Format duration + final hours = totalDuration ~/ 3600; + final minutes = ((totalDuration % 3600) / 60).round(); + duration.value = '${hours}h ${minutes}m'; + } else { + print('Google Directions API Error: ${data['status']}'); + } + } catch (e) { + print("Google route fetch error: $e"); + } + } + + Future fetchRouteWithWaypoints(List points) async { + final coordinates = points.map((p) => '${p.longitude},${p.latitude}').join(';'); + final url = Uri.parse('https://router.project-osrm.org/route/v1/driving/$coordinates?overview=full&geometries=geojson'); + + try { + final response = await http.get(url); + if (response.statusCode == 200) { + final decoded = json.decode(response.body); + final geometry = decoded['routes'][0]['geometry']['coordinates'] as List; + final dist = decoded['routes'][0]['distance']; + final dur = decoded['routes'][0]['duration']; + + routePoints.clear(); + routePoints.addAll(geometry.map((coord) => latlong.LatLng(coord[1], coord[0]))); + + if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { + distance.value = dist / 1000.00; + } else { + distance.value = dist / 1609.34; + } + + final hours = dur ~/ 3600; + final minutes = ((dur % 3600) / 60).round(); + duration.value = '${hours}h ${minutes}m'; + + // Zoom to fit polyline after drawing + zoomToPolylineOSM(); + } else { + print("Failed to get route: ${response.body}"); + } + } catch (e) { + print("Route fetch error: $e"); + } + } + + void zoomToPolylineOSM() { + if (routePoints.isEmpty) return; + // LatLngBounds requires at least two points + final bounds = flutterMap.LatLngBounds(routePoints.first, routePoints.first); + for (final point in routePoints) { + bounds.extend(point); + } + final center = bounds.center; + // Calculate zoom level to fit all points + double zoom = getBoundsZoomLevel(bounds); + mapOsmController.move(center, zoom); + } + + double getBoundsZoomLevel(flutterMap.LatLngBounds bounds) { + // Simple heuristic: zoom out for larger bounds + final latDiff = (bounds.northEast.latitude - bounds.southWest.latitude).abs(); + final lngDiff = (bounds.northEast.longitude - bounds.southWest.longitude).abs(); + double maxDiff = math.max(latDiff, lngDiff); + if (maxDiff < 0.005) return 18.0; + if (maxDiff < 0.01) return 16.0; + if (maxDiff < 0.05) return 14.0; + if (maxDiff < 0.1) return 12.0; + if (maxDiff < 0.5) return 10.0; + return 8.0; + } + + void addPolyLine(List points) { + final id = const PolylineId("poly"); + final polyline = Polyline(polylineId: id, color: AppThemeData.primary300, points: points, width: 6, geodesic: true); + polyLines[id] = polyline; + + if (points.length >= 2) { + // Zoom to fit all polyline points + updateCameraLocationToFitPolyline(points, mapController); + } + } + + Future updateCameraLocationToFitPolyline(List points, GoogleMapController? mapController) async { + if (mapController == null || points.isEmpty) return; + double minLat = points.first.latitude, maxLat = points.first.latitude; + double minLng = points.first.longitude, maxLng = points.first.longitude; + for (final p in points) { + if (p.latitude < minLat) minLat = p.latitude; + if (p.latitude > maxLat) maxLat = p.latitude; + if (p.longitude < minLng) minLng = p.longitude; + if (p.longitude > maxLng) maxLng = p.longitude; + } + final bounds = LatLngBounds(southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng)); + final cameraUpdate = CameraUpdate.newLatLngBounds(bounds, 50); + await checkCameraLocation(cameraUpdate, mapController); + } + + Future animateToSource(double lat, double long) async { + final hasBothCoords = departureLatLongOsm.value.latitude != 0.0 && destinationLatLongOsm.value.latitude != 0.0; + + if (hasBothCoords) { + await calculateZoomLevel(source: departureLatLongOsm.value, destination: destinationLatLongOsm.value); + } else { + mapOsmController.move(latlong.LatLng(lat, long), 10); + } + } + + RxMap polyLines = {}.obs; + + Future calculateZoomLevel({required latlong.LatLng source, required latlong.LatLng destination, double paddingFraction = 0.001}) async { + final bounds = flutterMap.LatLngBounds.fromPoints([source, destination]); + final screenSize = Size(Get.width, Get.height * 0.5); + const double worldDimension = 256.0; + const double maxZoom = 10.0; + + double latToRad(double lat) => math.log((1 + math.sin(lat * math.pi / 180)) / (1 - math.sin(lat * math.pi / 180))) / 2; + + double computeZoom(double screenPx, double worldPx, double fraction) => math.log(screenPx / worldPx / fraction) / math.ln2; + + final north = bounds.northEast.latitude; + final south = bounds.southWest.latitude; + final east = bounds.northEast.longitude; + final west = bounds.southWest.longitude; + + final latDelta = (north - south).abs(); + final lngDelta = (east - west).abs(); + + final center = bounds.center; + + if (latDelta < 1e-6 || lngDelta < 1e-6) { + mapOsmController.move(center, maxZoom); + } else { + final latFraction = (latToRad(north) - latToRad(south)) / math.pi; + final lngFraction = ((east - west + 360) % 360) / 360; + + final latZoom = computeZoom(screenSize.height, worldDimension, latFraction + paddingFraction); + final lngZoom = computeZoom(screenSize.width, worldDimension, lngFraction + paddingFraction); + + final zoomLevel = math.min(latZoom, lngZoom).clamp(0.0, maxZoom); + mapOsmController.move(center, zoomLevel); + } + } + + Future updateCameraLocation(LatLng source, LatLng destination, GoogleMapController? mapController) async { + if (mapController == null) return; + + final bounds = LatLngBounds( + southwest: LatLng(math.min(source.latitude, destination.latitude), math.min(source.longitude, destination.longitude)), + northeast: LatLng(math.max(source.latitude, destination.latitude), math.max(source.longitude, destination.longitude)), + ); + + final cameraUpdate = CameraUpdate.newLatLngBounds(bounds, 90); + await checkCameraLocation(cameraUpdate, mapController); + } + + Future checkCameraLocation(CameraUpdate cameraUpdate, GoogleMapController mapController) async { + await mapController.animateCamera(cameraUpdate); + final l1 = await mapController.getVisibleRegion(); + final l2 = await mapController.getVisibleRegion(); + + if (l1.southwest.latitude == -90 || l2.southwest.latitude == -90) { + await checkCameraLocation(cameraUpdate, mapController); + } + } + + Future setIcons() async { + try { + if (Constant.selectedMapType == 'osm') { + departureIconOsm = Image.asset("assets/icons/pickup.png", width: 30, height: 30); + destinationIconOsm = Image.asset("assets/icons/dropoff.png", width: 30, height: 30); + taxiIconOsm = Image.asset("assets/icons/ic_taxi.png", width: 30, height: 30); + stopIconOsm = Image.asset("assets/icons/location.png", width: 26, height: 26); + } else { + const config = ImageConfiguration(size: Size(48, 48)); + departureIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/pickup.png"); + destinationIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/dropoff.png"); + taxiIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/ic_taxi.png"); + stopIcon = await BitmapDescriptor.fromAssetImage(config, "assets/icons/location.png"); + } + } catch (e) { + print('Error loading icons: $e'); + } + } + + void clearMapDataIfLocationsRemoved() { + final isSourceEmpty = departureLatLongOsm.value.latitude == 0.0 && departureLatLongOsm.value.longitude == 0.0; + final isDestinationEmpty = destinationLatLongOsm.value.latitude == 0.0 && destinationLatLongOsm.value.longitude == 0.0; + + if (isSourceEmpty || isDestinationEmpty) { + // Clear polylines + polyLines.clear(); + + // Clear OSM markers (if using OSM) + osmMarker.clear(); + + // Clear Google markers (if using Google Maps) + markers.clear(); + + // Clear route points (optional) + routePoints.clear(); + + // Reset distance and duration values + distance.value = 0.0; + duration.value = ''; + } + } + + void removeSource() { + // Clear departure location and related data + departureLatLongOsm.value = latlong.LatLng(0.0, 0.0); + departureLatLong.value = const LatLng(0.0, 0.0); + sourceTextEditController.value.clear(); + + // Remove marker + if (Constant.selectedMapType == 'osm') { + osmMarker.removeWhere((marker) => marker.point == departureLatLongOsm.value); + } else { + markers.removeWhere((marker) => marker.markerId.value == 'Departure'); + } + + // Clear polylines and route info if needed + clearMapDataIfLocationsRemoved(); + update(); + } + + void removeDestination() { + destinationLatLongOsm.value = latlong.LatLng(0.0, 0.0); + destinationLatLong.value = const LatLng(0.0, 0.0); + destinationTextEditController.value.clear(); + + if (Constant.selectedMapType == 'osm') { + osmMarker.removeWhere((marker) => marker.point == destinationLatLongOsm.value); + } else { + markers.removeWhere((marker) => marker.markerId.value == 'Destination'); + } + + clearMapDataIfLocationsRemoved(); + update(); + } + + Future searchPlaceNameOSM() async { + final url = Uri.parse('https://nominatim.openstreetmap.org/reverse?lat=${departureLatLongOsm.value.latitude}&lon=${departureLatLongOsm.value.longitude}&format=json'); + + final response = await http.get(url, headers: {'User-Agent': 'FlutterMapApp/1.0 (menil.siddhiinfosoft@gmail.com)'}); + + if (response.statusCode == 200) { + log("response.body :: ${response.body}"); + Map data = json.decode(response.body); + sourceTextEditController.value.text = data['display_name'] ?? ''; + } + } + + Future searchPlaceNameGoogle() async { + final lat = departureLatLong.value.latitude; + final lng = departureLatLong.value.longitude; + + final url = Uri.parse('https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lng&key=${Constant.mapAPIKey}'); + + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == 'OK') { + final results = data['results'] as List; + if (results.isNotEmpty) { + final formattedAddress = results[0]['formatted_address']; + sourceTextEditController.value.text = formattedAddress; + } + } else { + log("Google API Error: ${data['status']}"); + } + } else { + log("HTTP Error: ${response.statusCode}"); + } + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'eMart Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": userModel.value.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": userModel.value.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + completeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen(amount: (double.parse(totalAmount) * 100).toString(), currency: "ZAR", secretKey: payStackModel.value.secretKey.toString(), userModel: userModel.value).then(( + value, + ) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": userModel.value.email.toString(), + "phonenumber": userModel.value.phoneNumber, // Add a real phone number + "name": userModel.value.fullName(), // Add a real customer name + }, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: userModel.value).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + // final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + // String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + // + // final response = await http.post( + // Uri.parse(getChecksum), + // headers: {}, + // body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + // ); + // + // final data = jsonDecode(response.body); + // await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + // initiatePayment(amount: amount, orderId: orderId).then((value) { + // String callback = ""; + // if (paytmModel.value.isSandboxEnabled == true) { + // callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } else { + // callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } + // + // GetPaymentTxtTokenModel result = value; + // startTransaction(context, txnTokenBy: result.body.txnToken, orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + // }); + // }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // completeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), "checksum_value": checkSum}, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post( + Uri.parse(initiateURL), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }, + ); + log(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': userModel.value.phoneNumber, 'email': userModel.value.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + // Handle the response + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + // ignore: use_build_context_synchronously + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/cab_coupon_code_controller.dart b/lib/controllers/cab_coupon_code_controller.dart new file mode 100644 index 0000000..8d57707 --- /dev/null +++ b/lib/controllers/cab_coupon_code_controller.dart @@ -0,0 +1,30 @@ +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class CabCouponCodeController extends GetxController { + // Add your methods and properties here + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + + void getData(){ + getCouponCode(); + } + RxBool isLoading = true.obs; + RxList cabCouponList = [].obs; + + Future getCouponCode() async { + await FireStoreUtils.getCabCoupon().then((value) { + cabCouponList.value = value; + // Handle the retrieved coupon code + }); + print("cabCouponList ${cabCouponList.length}"); + isLoading.value = false; + } +} diff --git a/lib/controllers/cab_dashboard_controller.dart b/lib/controllers/cab_dashboard_controller.dart new file mode 100644 index 0000000..309907f --- /dev/null +++ b/lib/controllers/cab_dashboard_controller.dart @@ -0,0 +1,33 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/cab_service_screens/cab_home_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; +import '../screen_ui/cab_service_screens/my_cab_booking_screen.dart'; + +class CabDashboardController extends GetxController { + RxInt selectedIndex = 0.obs; + + RxList pageList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getTaxList(); + if (Constant.walletSetting == false) { + pageList.value = [CabHomeScreen(), const MyCabBookingScreen(), const ProfileScreen()]; + } else { + pageList.value = [CabHomeScreen(), const MyCabBookingScreen(), const WalletScreen(), const ProfileScreen()]; + } + super.onInit(); + } + + Future getTaxList() async { + await FireStoreUtils.getTaxList(Constant.sectionConstantModel!.id).then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + } +} diff --git a/lib/controllers/cab_home_controller.dart b/lib/controllers/cab_home_controller.dart new file mode 100644 index 0000000..ba4361c --- /dev/null +++ b/lib/controllers/cab_home_controller.dart @@ -0,0 +1,22 @@ +import 'package:customer/models/banner_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class CabHomeController extends GetxController { + RxBool isLoading = true.obs; + RxList bannerTopHome = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + Future getData() async { + await FireStoreUtils.getHomeTopBanner().then((value) { + bannerTopHome.value = value; + }); + isLoading.value = false; + } +} diff --git a/lib/controllers/cab_order_details_controller.dart b/lib/controllers/cab_order_details_controller.dart new file mode 100644 index 0000000..0106e98 --- /dev/null +++ b/lib/controllers/cab_order_details_controller.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/rating_model.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:google_maps_flutter/google_maps_flutter.dart' as gmap; +import 'package:latlong2/latlong.dart' as osm; +import '../models/cab_order_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/app_them_data.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; + +class CabOrderDetailsController extends GetxController { + Rx cabOrder = CabOrderModel().obs; + + RxBool isLoading = false.obs; + + // Google Maps Data + RxSet googleMarkers = {}.obs; + RxSet googlePolylines = {}.obs; + + // OSM Data + RxList osmPolyline = [].obs; + + final String googleApiKey = Constant.mapAPIKey; + + final Rx driverUser = UserModel().obs; + Rx ratingModel = RatingModel().obs; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + if (args != null) { + cabOrder.value = args['cabOrderModel'] as CabOrderModel; + calculateTotalAmount(); + _setMarkers(); + _getGoogleRoute(); + _getOsmRoute(); + } + fetchDriverDetails(); + } + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + String formatDate(Timestamp timestamp) { + final dateTime = timestamp.toDate(); + return DateFormat("dd MMM yyyy, hh:mm a").format(dateTime); + } + + Future fetchDriverDetails() async { + if (cabOrder.value.driverId != null) { + await FireStoreUtils.getUserProfile(cabOrder.value.driverId ?? '').then((value) { + if (value != null) { + driverUser.value = value; + } + }); + + print(driverUser.value.toJson()); + await FireStoreUtils.getReviewsbyID(cabOrder.value.id.toString()).then((value) { + if (value != null) { + ratingModel.value = value; + } + }); + } + } + + void calculateTotalAmount() { + taxAmount = 0.0.obs; + discount = 0.0.obs; + subTotal.value = double.parse(cabOrder.value.subTotal.toString()); + discount.value = double.parse(cabOrder.value.discount ?? '0.0'); + + if (cabOrder.value.taxSetting != null) { + for (var element in cabOrder.value.taxSetting!) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + update(); + } + + void _setMarkers() { + final sourceLat = cabOrder.value.sourceLocation!.latitude; + final sourceLng = cabOrder.value.sourceLocation!.longitude; + final destLat = cabOrder.value.destinationLocation!.latitude; + final destLng = cabOrder.value.destinationLocation!.longitude; + + googleMarkers.value = { + gmap.Marker(markerId: const gmap.MarkerId('source'), position: gmap.LatLng(sourceLat!, sourceLng!), icon: gmap.BitmapDescriptor.defaultMarkerWithHue(gmap.BitmapDescriptor.hueGreen)), + gmap.Marker(markerId: const gmap.MarkerId('destination'), position: gmap.LatLng(destLat!, destLng!), icon: gmap.BitmapDescriptor.defaultMarkerWithHue(gmap.BitmapDescriptor.hueRed)), + }; + } + + ///Google Directions API + Future _getGoogleRoute() async { + final src = cabOrder.value.sourceLocation; + final dest = cabOrder.value.destinationLocation; + + final url = "https://maps.googleapis.com/maps/api/directions/json?origin=${src!.latitude},${src.longitude}&destination=${dest!.latitude},${dest.longitude}&key=$googleApiKey"; + + final response = await http.get(Uri.parse(url)); + final data = jsonDecode(response.body); + + if (data["routes"].isNotEmpty) { + final points = data["routes"][0]["overview_polyline"]["points"]; + final polylinePoints = PolylinePoints.decodePolyline(points); + + final polylineCoords = polylinePoints.map((p) => gmap.LatLng(p.latitude, p.longitude)).toList(); + + googlePolylines.value = {gmap.Polyline(polylineId: const gmap.PolylineId("google_route"), color: AppThemeData.onDemandDark100, width: 5, points: polylineCoords)}; + } + } + + /// OSM Route (OSRM API) + Future _getOsmRoute() async { + final src = cabOrder.value.sourceLocation; + final dest = cabOrder.value.destinationLocation; + + final url = "http://router.project-osrm.org/route/v1/driving/${src!.longitude},${src.latitude};${dest!.longitude},${dest.latitude}?overview=full&geometries=geojson"; + + final response = await http.get(Uri.parse(url)); + final data = jsonDecode(response.body); + + if (data["routes"].isNotEmpty) { + final coords = data["routes"][0]["geometry"]["coordinates"] as List; + + osmPolyline.value = coords.map((c) => osm.LatLng(c[1].toDouble(), c[0].toDouble())).toList(); + } + } +} diff --git a/lib/controllers/cab_rental_dashboard_controllers.dart b/lib/controllers/cab_rental_dashboard_controllers.dart new file mode 100644 index 0000000..add5017 --- /dev/null +++ b/lib/controllers/cab_rental_dashboard_controllers.dart @@ -0,0 +1,34 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/screen_ui/rental_service/rental_home_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +import '../screen_ui/rental_service/my_rental_booking_screen.dart'; + +class CabRentalDashboardControllers extends GetxController { + RxInt selectedIndex = 0.obs; + + RxList pageList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getTaxList(); + if (Constant.walletSetting == false) { + pageList.value = [RentalHomeScreen(), MyRentalBookingScreen(), const ProfileScreen()]; + } else { + pageList.value = [RentalHomeScreen(), MyRentalBookingScreen(), const WalletScreen(), const ProfileScreen()]; + } + super.onInit(); + } + + Future getTaxList() async { + await FireStoreUtils.getTaxList(Constant.sectionConstantModel!.id).then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + } +} diff --git a/lib/controllers/cab_review_controller.dart b/lib/controllers/cab_review_controller.dart new file mode 100644 index 0000000..8815b61 --- /dev/null +++ b/lib/controllers/cab_review_controller.dart @@ -0,0 +1,133 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/cab_order_model.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../constant/collection_name.dart'; +import '../models/rating_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; +import '../constant/constant.dart'; +import '../themes/show_toast_dialog.dart'; + +class CabReviewController extends GetxController { + RxBool isLoading = true.obs; + + final Rx order = Rx(null); + + final Rx ratingModel = Rx(null); + final RxDouble ratings = 0.0.obs; + final Rx comment = TextEditingController().obs; + + final Rx driverUser = Rx(null); + + final RxInt futureCount = 0.obs; + final RxInt futureSum = 0.obs; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + if (args != null && args['order'] != null) { + order.value = args['order'] as CabOrderModel; + getReview(); + } + } + + /// Fetch old review + driver stats + Future getReview() async { + await FireStoreUtils.getReviewsbyID(order.value?.id ?? "").then((value) { + if (value != null) { + ratingModel.value = value; + ratings.value = value.rating ?? 0; + comment.value.text = value.comment ?? ""; + } + }); + + await FireStoreUtils.getUserProfile(order.value?.driverId ?? '').then((value) { + if (value != null) { + driverUser.value = value; + + final int userReviewsCount = int.tryParse(driverUser.value!.reviewsCount?.toString() ?? "0") ?? 0; + final int userReviewsSum = int.tryParse(driverUser.value!.reviewsSum?.toString() ?? "0") ?? 0; + + if (ratingModel.value != null) { + final int oldRating = ratingModel.value?.rating?.toInt() ?? 0; + futureCount.value = userReviewsCount - 1; + futureSum.value = userReviewsSum - oldRating; + } else { + futureCount.value = userReviewsCount; + futureSum.value = userReviewsSum; + } + } + }); + + isLoading.value = false; + } + + /// Save / update review + Future submitReview() async { + if (comment.value.text.trim().isEmpty || ratings.value == 0) { + ShowToastDialog.showToast("Please provide rating and comment".tr); + return; + } + + ShowToastDialog.showLoader("Submit in...".tr); + + final user = await FireStoreUtils.getUserProfile(order.value?.driverId ?? ''); + + if (user != null) { + user.reviewsCount = (futureCount.value + 1).toString(); + user.reviewsSum = (futureSum.value + ratings.value.toInt()).toString(); + } + if (ratingModel.value != null) { + /// Update existing review + final updatedRating = RatingModel( + id: ratingModel.value!.id, + comment: comment.value.text, + photos: ratingModel.value?.photos ?? [], + rating: ratings.value, + orderId: ratingModel.value!.orderId, + driverId: ratingModel.value!.driverId, + customerId: ratingModel.value!.customerId, + vendorId: ratingModel.value?.vendorId, + uname: "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(updatedRating); + if (user != null) { + await FireStoreUtils.updateUser(user); + } + } else { + /// New review + final docRef = FireStoreUtils.fireStore.collection(CollectionName.itemsReview).doc(); + final newRating = RatingModel( + id: docRef.id, + comment: comment.value.text, + photos: [], + rating: ratings.value, + orderId: order.value?.id, + driverId: order.value?.driverId.toString(), + customerId: Constant.userModel?.id, + uname: "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(newRating); + if (user != null) { + await FireStoreUtils.updateUser(user); + } + } + + ShowToastDialog.closeLoader(); + Get.back(result: true); + } + + @override + void onClose() { + comment.value.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/cart_controller.dart b/lib/controllers/cart_controller.dart new file mode 100644 index 0000000..4335f0f --- /dev/null +++ b/lib/controllers/cart_controller.dart @@ -0,0 +1,1113 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as maths; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/order_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; + +import '../models/cashback_model.dart'; +import '../models/cashback_redeem_model.dart'; +import '../models/payment_model/cod_setting_model.dart'; +import '../models/payment_model/flutter_wave_model.dart'; +import '../models/payment_model/mercado_pago_model.dart'; +import '../models/payment_model/mid_trans.dart'; +import '../models/payment_model/orange_money.dart'; +import '../models/payment_model/pay_fast_model.dart'; +import '../models/payment_model/pay_stack_model.dart'; +import '../models/payment_model/paypal_model.dart'; +import '../models/payment_model/paytm_model.dart'; +import '../models/payment_model/razorpay_model.dart'; +import '../models/payment_model/stripe_model.dart'; +import '../models/payment_model/wallet_setting_model.dart'; +import '../models/payment_model/xendit.dart'; +import '../models/wallet_transaction_model.dart'; +import '../payment/MercadoPagoScreen.dart'; +import '../payment/PayFastScreen.dart'; +import '../payment/getPaytmTxtToken.dart'; +import '../payment/midtrans_screen.dart'; +import '../payment/orangePayScreen.dart'; +import '../payment/paystack/pay_stack_screen.dart'; +import '../payment/paystack/pay_stack_url_model.dart'; +import '../payment/paystack/paystack_url_genrater.dart'; +import '../payment/stripe_failed_model.dart'; +import '../payment/xenditModel.dart'; +import '../payment/xenditScreen.dart'; +import '../screen_ui/multi_vendor_service/cart_screen/oder_placing_screens.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../service/cart_provider.dart'; +import '../service/fire_store_utils.dart'; +import '../service/send_notification.dart'; +import '../themes/show_toast_dialog.dart'; + +class CartController extends GetxController { + RxBool isCashbackApply = false.obs; + Rx bestCashback = CashbackModel().obs; + + final CartProvider cartProvider = CartProvider(); + Rx reMarkController = TextEditingController().obs; + Rx couponCodeController = TextEditingController().obs; + Rx tipsController = TextEditingController().obs; + + Rx selectedAddress = ShippingAddress().obs; + Rx vendorModel = VendorModel().obs; + Rx deliveryChargeModel = DeliveryCharge().obs; + Rx userModel = UserModel().obs; + RxList couponList = [].obs; + RxList allCouponList = [].obs; + RxString selectedFoodType = "Delivery".obs; + + RxString selectedPaymentMethod = ''.obs; + RxBool isOrderPlaced = false.obs; + + RxString deliveryType = "instant".obs; + Rx scheduleDateTime = DateTime.now().obs; + RxDouble totalDistance = 0.0.obs; + RxDouble deliveryCharges = 0.0.obs; + RxDouble subTotal = 0.0.obs; + RxDouble couponAmount = 0.0.obs; + + RxDouble specialDiscountAmount = 0.0.obs; + RxDouble specialDiscount = 0.0.obs; + RxString specialType = "".obs; + + RxDouble deliveryTips = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + Rx selectedCouponModel = CouponModel().obs; + + @override + void onInit() { + // TODO: implement onInit + selectedAddress.value = Constant.selectedLocation; + getCartData(); + getPaymentSettings(); + super.onInit(); + } + + Future getCartData() async { + cartProvider.cartStream.listen((event) async { + cartItem.clear(); + cartItem.addAll(event); + + if (cartItem.isNotEmpty) { + await FireStoreUtils.getVendorById(cartItem.first.vendorID.toString()).then((value) { + if (value != null) { + vendorModel.value = value; + } + }); + } + calculatePrice(); + }); + selectedFoodType.value = Preferences.getString(Preferences.foodDeliveryType, defaultValue: "Delivery"); + + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) { + if (value != null) { + userModel.value = value; + } + }); + + await FireStoreUtils.getDeliveryCharge().then((value) { + if (value != null) { + deliveryChargeModel.value = value; + print("===> Delivery Charge Model: ${deliveryChargeModel.value.toJson()}"); + calculatePrice(); + } + }); + + await FireStoreUtils.getAllVendorPublicCoupons(vendorModel.value.id.toString()).then((value) { + couponList.value = value; + }); + + await FireStoreUtils.getAllVendorCoupons(vendorModel.value.id.toString()).then((value) { + allCouponList.value = value; + }); + } + + Future calculatePrice() async { + deliveryCharges.value = 0.0; + subTotal.value = 0.0; + couponAmount.value = 0.0; + specialDiscountAmount.value = 0.0; + taxAmount.value = 0.0; + totalAmount.value = 0.0; + + if (cartItem.isNotEmpty) { + if (Constant.sectionConstantModel!.serviceTypeFlag == "ecommerce-service") { + deliveryCharges.value = double.parse(Constant.sectionConstantModel!.delivery_charge ?? "0"); + } else { + if (selectedFoodType.value == "Delivery") { + totalDistance.value = double.parse( + Constant.getDistance( + lat1: selectedAddress.value.location!.latitude.toString(), + lng1: selectedAddress.value.location!.longitude.toString(), + lat2: vendorModel.value.latitude.toString(), + lng2: vendorModel.value.longitude.toString(), + ), + ); + if (vendorModel.value.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true) { + deliveryCharges.value = 0.0; + } else if (deliveryChargeModel.value.vendorCanModify == false) { + if (totalDistance.value > deliveryChargeModel.value.minimumDeliveryChargesWithinKm!) { + deliveryCharges.value = totalDistance.value * deliveryChargeModel.value.deliveryChargesPerKm!; + } else { + deliveryCharges.value = (deliveryChargeModel.value.minimumDeliveryCharges)!.toDouble(); + } + } else { + if (vendorModel.value.deliveryCharge != null) { + if (totalDistance.value > vendorModel.value.deliveryCharge!.minimumDeliveryChargesWithinKm!) { + deliveryCharges.value = (totalDistance.value * vendorModel.value.deliveryCharge!.deliveryChargesPerKm!).toDouble(); + } else { + deliveryCharges.value = vendorModel.value.deliveryCharge!.minimumDeliveryCharges!.toDouble(); + } + } else { + if (totalDistance.value > deliveryChargeModel.value.minimumDeliveryChargesWithinKm!) { + deliveryCharges.value = (totalDistance.value * deliveryChargeModel.value.deliveryChargesPerKm!).toDouble(); + } else { + deliveryCharges.value = deliveryChargeModel.value.minimumDeliveryCharges!.toDouble(); + } + } + } + } else { + deliveryCharges.value = 0.0; + } + } + } + + for (var element in cartItem) { + if (double.parse(element.discountPrice.toString()) <= 0) { + subTotal.value = + subTotal.value + + double.parse(element.price.toString()) * double.parse(element.quantity.toString()) + + (double.parse(element.extrasPrice.toString()) * double.parse(element.quantity.toString())); + } else { + subTotal.value = + subTotal.value + + double.parse(element.discountPrice.toString()) * double.parse(element.quantity.toString()) + + (double.parse(element.extrasPrice.toString()) * double.parse(element.quantity.toString())); + } + } + + if (selectedCouponModel.value.id != null) { + couponAmount.value = Constant.calculateDiscount(amount: subTotal.value.toString(), offerModel: selectedCouponModel.value); + } + + if (vendorModel.value.specialDiscountEnable == true && Constant.specialDiscountOffer == true) { + final now = DateTime.now(); + var day = DateFormat('EEEE', 'en_US').format(now); + var date = DateFormat('dd-MM-yyyy').format(now); + for (var element in vendorModel.value.specialDiscount!) { + if (day == element.day.toString()) { + if (element.timeslot!.isNotEmpty) { + for (var element in element.timeslot!) { + if (element.discountType == "delivery") { + var start = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.from}"); + var end = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.to}"); + if (isCurrentDateInRange(start, end)) { + specialDiscount.value = double.parse(element.discount.toString()); + specialType.value = element.type.toString(); + if (element.type == "percentage") { + specialDiscountAmount.value = subTotal * specialDiscount.value / 100; + } else { + specialDiscountAmount.value = specialDiscount.value; + } + } + } + } + } + } + } + } else { + specialDiscount.value = double.parse("0"); + specialType.value = "amount"; + } + + for (var element in Constant.taxList) { + taxAmount.value = taxAmount.value + Constant.calculateTax(amount: (subTotal.value - couponAmount.value - specialDiscountAmount.value).toString(), taxModel: element); + } + + totalAmount.value = (subTotal.value - couponAmount.value - specialDiscountAmount.value) + taxAmount.value + deliveryCharges.value + deliveryTips.value; + getCashback(); + } + + Future getCashback() async { + if (Constant.isCashbackActive == true) { + final paymentMethod = selectedPaymentMethod.value; + final orderTotal = subTotal.value; + final now = DateTime.now(); + + List eligibleCashbacks = []; + double maxCashbackValue = 0.0; + + final cashbackModelList = await FireStoreUtils.getAllCashbak(); + + for (final cashback in cashbackModelList) { + final startDate = cashback.startDate; + final endDate = cashback.endDate; + + if (startDate == null || endDate == null) continue; + + final withinDateRange = startDate.toDate().isBefore(now) && endDate.toDate().isAfter(now); + final meetsMinAmount = orderTotal >= (cashback.minimumPurchaseAmount ?? 0); + final allPayment = cashback.allPayment ?? false; + final paymentMatch = allPayment || (cashback.paymentMethods ?? []).contains(paymentMethod); + final allCustomer = cashback.allCustomer ?? false; + final customerMatch = allCustomer || (cashback.customerIds ?? []).contains(FireStoreUtils.getCurrentUid()); + + final redeemData = await FireStoreUtils.getRedeemedCashbacks(cashback.id ?? ''); + final underLimit = redeemData.length < (cashback.redeemLimit ?? 0); + + if (withinDateRange && meetsMinAmount && paymentMatch && customerMatch && underLimit) { + eligibleCashbacks.add(cashback); + } + } + bestCashback.value = CashbackModel(); + for (final cashback in eligibleCashbacks) { + double cashbackValue = 0.0; + + if (cashback.cashbackType == 'Percent') { + final percentage = cashback.cashbackAmount ?? 0.0; + cashbackValue = (percentage / 100.0) * orderTotal; + } else if (cashback.cashbackType == 'Fixed') { + cashbackValue = cashback.cashbackAmount ?? 0.0; + } + + final maxDiscount = cashback.maximumDiscount ?? cashbackValue; + if (cashbackValue > maxDiscount) cashbackValue = maxDiscount; + + if (cashbackValue > maxCashbackValue) { + maxCashbackValue = cashbackValue; + bestCashback.value = cashback; + } + } + + if (bestCashback.value.id != null) { + final cashbackValue = maxCashbackValue; + isCashbackApply.value = true; + bestCashback.value.cashbackValue = cashbackValue; + } else { + bestCashback.value = CashbackModel(); + isCashbackApply.value = false; + } + } else { + bestCashback.value = CashbackModel(); + isCashbackApply.value = false; + } + } + + Future addToCart({required CartProductModel cartProductModel, required bool isIncrement, required int quantity}) async { + if (isIncrement) { + cartProvider.addToCart(Get.context!, cartProductModel, quantity); + } else { + cartProvider.removeFromCart(cartProductModel, quantity); + } + update(); + } + + List tempProduc = []; + + Future placeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + if (double.parse(userModel.value.walletAmount.toString()) >= totalAmount.value) { + setOrder(); + } else { + ShowToastDialog.showToast("You don't have sufficient wallet balance to place order".tr); + } + } else { + setOrder(); + } + } + + Future setOrder() async { + ShowToastDialog.showLoader("Please wait...".tr); + + if ((Constant.isSubscriptionModelApplied == true || Constant.sectionConstantModel?.adminCommision?.isEnabled == true) && vendorModel.value.subscriptionPlan != null) { + await FireStoreUtils.getVendorById(vendorModel.value.id!).then((vender) async { + if (vender?.subscriptionTotalOrders == '0' || vender?.subscriptionTotalOrders == null) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("This vendor has reached their maximum order capacity. Please select a different vendor or try again later.".tr); + return; + } + }); + } + + for (CartProductModel cartProduct in cartItem) { + CartProductModel tempCart = cartProduct; + if (cartProduct.extrasPrice == '0') { + tempCart.extras = []; + } + tempProduc.add(tempCart); + } + + Map specialDiscountMap = {'special_discount': specialDiscountAmount.value, 'special_discount_label': specialDiscount.value, 'specialType': specialType.value}; + + OrderModel orderModel = OrderModel(); + orderModel.id = Constant.getUuid(); + orderModel.address = selectedAddress.value; + orderModel.authorID = FireStoreUtils.getCurrentUid(); + orderModel.author = userModel.value; + orderModel.vendorID = vendorModel.value.id; + orderModel.vendor = vendorModel.value; + orderModel.adminCommission = + Constant.sectionConstantModel?.adminCommision?.isEnabled == false + ? '0' + : vendorModel.value.adminCommission != null + ? vendorModel.value.adminCommission!.amount.toString() + : Constant.sectionConstantModel?.adminCommision?.amount.toString(); + orderModel.adminCommissionType = + Constant.sectionConstantModel?.adminCommision?.isEnabled == false + ? 'fixed' + : vendorModel.value.adminCommission != null + ? vendorModel.value.adminCommission!.commissionType + : Constant.sectionConstantModel?.adminCommision?.commissionType; + orderModel.status = Constant.orderPlaced; + orderModel.discount = couponAmount.value; + orderModel.couponId = selectedCouponModel.value.id; + orderModel.taxSetting = Constant.taxList; + orderModel.paymentMethod = selectedPaymentMethod.value; + orderModel.products = cartItem; + orderModel.sectionId = Constant.sectionConstantModel?.id; + orderModel.specialDiscount = specialDiscountMap; + orderModel.couponCode = selectedCouponModel.value.code; + orderModel.deliveryCharge = deliveryCharges.value.toString(); + orderModel.tipAmount = deliveryTips.value.toString(); + orderModel.notes = reMarkController.value.text; + orderModel.takeAway = selectedFoodType.value == "Delivery" ? false : true; + orderModel.createdAt = Timestamp.now(); + orderModel.scheduleTime = deliveryType.value == "schedule" ? Timestamp.fromDate(scheduleDateTime.value) : null; + orderModel.cashback = bestCashback.value.id == null ? null : bestCashback.value; + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.value.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: orderModel.id, + note: "Order Amount debited".tr, + paymentStatus: "success".tr, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()).then((value) {}); + } + }); + } + + for (int i = 0; i < tempProduc.length; i++) { + await FireStoreUtils.getProductById(tempProduc[i].id!.split('~').first).then((value) async { + ProductModel? productModel = value; + if (tempProduc[i].variantInfo != null) { + if (productModel!.itemAttribute != null) { + for (int j = 0; j < productModel.itemAttribute!.variants!.length; j++) { + if (productModel.itemAttribute!.variants![j].variantId == tempProduc[i].id!.split('~').last) { + if (productModel.itemAttribute!.variants![j].variantQuantity != "-1") { + productModel.itemAttribute!.variants![j].variantQuantity = (int.parse(productModel.itemAttribute!.variants![j].variantQuantity.toString()) - tempProduc[i].quantity!).toString(); + } + } + } + } else { + if (productModel.quantity != -1) { + productModel.quantity = (productModel.quantity! - tempProduc[i].quantity!); + } + } + } else { + if (productModel!.quantity != -1) { + productModel.quantity = (productModel.quantity! - tempProduc[i].quantity!); + } + } + + await FireStoreUtils.setProduct(productModel); + }); + } + if (Constant.isCashbackActive == true && bestCashback.value.id != null) { + CashbackRedeemModel cashbackRedeemModel = CashbackRedeemModel( + id: Constant.getUuid(), + cashbackId: bestCashback.value.id, + userId: FireStoreUtils.getCurrentUid(), + orderId: orderModel.id, + createdAt: Timestamp.now(), + ); + await FireStoreUtils.setCashbackRedeemModel(cashbackRedeemModel); + } + await FireStoreUtils.setOrder(orderModel).then((value) async { + await FireStoreUtils.getUserProfile(orderModel.vendor!.author.toString()).then((value) async { + if (value != null) { + if (orderModel.scheduleTime != null) { + await SendNotification.sendFcmMessage(Constant.scheduleOrder, value.fcmToken ?? '', {}); + } else { + await SendNotification.sendFcmMessage(Constant.newOrderPlaced, value.fcmToken ?? '', {}); + } + } + }); + await Constant.sendOrderEmail(orderModel: orderModel); + ShowToastDialog.closeLoader(); + Get.off(const OrderPlacingScreen(), arguments: {"orderModel": orderModel}); + }); + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'Foodie Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": userModel.value.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": userModel.value.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + placeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen(amount: (double.parse(totalAmount) * 100).toString(), currency: "ZAR", secretKey: payStackModel.value.secretKey.toString(), userModel: userModel.value).then(( + value, + ) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": userModel.value.email.toString(), + "phonenumber": userModel.value.phoneNumber, // Add a real phone number + "name": userModel.value.fullName(), // Add a real customer name + }, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: userModel.value).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + ); + + final data = jsonDecode(response.body); + await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + initiatePayment(amount: amount, orderId: orderId).then((value) { + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + + GetPaymentTxtTokenModel result = value; + startTransaction(context, txnTokenBy: result.body.txnToken ?? '', orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + }); + }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // placeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), "checksum_value": checkSum}, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post( + Uri.parse(initiateURL), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }, + ); + log(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': userModel.value.phoneNumber, 'email': userModel.value.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + (); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + // Handle the response + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + // ignore: use_build_context_synchronously + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/cashback_controller.dart b/lib/controllers/cashback_controller.dart new file mode 100644 index 0000000..43aa518 --- /dev/null +++ b/lib/controllers/cashback_controller.dart @@ -0,0 +1,24 @@ +import '../models/cashback_model.dart'; +import 'package:get/get.dart'; +import '../service/fire_store_utils.dart'; + +class CashbackController extends GetxController { + RxList cashbackList = [].obs; + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getCashback(); + super.onInit(); + } + + Future getCashback() async { + await FireStoreUtils.getCashbackList().then((value) { + if (value.isNotEmpty) { + cashbackList.value = value; + } + }); + isLoading.value = false; + } +} diff --git a/lib/controllers/category_restaurant_controller.dart b/lib/controllers/category_restaurant_controller.dart new file mode 100644 index 0000000..bd8e41a --- /dev/null +++ b/lib/controllers/category_restaurant_controller.dart @@ -0,0 +1,62 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import '../models/vendor_category_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class CategoryRestaurantController extends GetxController { + RxBool isLoading = true.obs; + RxBool dineIn = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Rx vendorCategoryModel = VendorCategoryModel().obs; + RxList allNearestRestaurant = [].obs; + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorCategoryModel.value = argumentData['vendorCategoryModel']; + dineIn.value = argumentData['dineIn']; + await getZone(); + await getRestaurant(); + } + Future.delayed(Duration(seconds: 1), () { + isLoading.value = false; + }); + } + + Future getRestaurant() async { + FireStoreUtils.getAllNearestRestaurantByCategoryId(categoryId: vendorCategoryModel.value.id.toString(), isDining: dineIn.value).listen(( + event, + ) async { + allNearestRestaurant.clear(); + allNearestRestaurant.addAll(event); + }); + } + + Future getZone() async { + await FireStoreUtils.getZone().then((value) { + if (value != null) { + for (int i = 0; i < value.length; i++) { + if (Constant.isPointInPolygon( + LatLng(Constant.selectedLocation.location!.latitude ?? 0.0, Constant.selectedLocation.location!.longitude ?? 0.0), + value[i].area!, + )) { + Constant.selectedZone = value[i]; + Constant.isZoneAvailable = true; + break; + } else { + Constant.isZoneAvailable = false; + } + } + } + }); + } +} diff --git a/lib/controllers/change_language_controller.dart b/lib/controllers/change_language_controller.dart new file mode 100644 index 0000000..aad7891 --- /dev/null +++ b/lib/controllers/change_language_controller.dart @@ -0,0 +1,44 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/language_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:get/get.dart'; + +import '../constant/collection_name.dart'; + +class ChangeLanguageController extends GetxController { + Rx selectedLanguage = LanguageModel().obs; + RxList languageList = [].obs; + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getLanguage(); + + super.onInit(); + } + + Future getLanguage() async { + await FireStoreUtils.fireStore.collection(CollectionName.settings).doc("languages").get().then((event) { + if (event.exists) { + List languageListTemp = event.data()!["list"]; + for (var element in languageListTemp) { + LanguageModel languageModel = LanguageModel.fromJson(element); + languageList.add(languageModel); + } + + if (Preferences.getString(Preferences.languageCodeKey).toString().isNotEmpty) { + LanguageModel pref = Constant.getLanguage(); + for (var element in languageList) { + if (element.slug == pref.slug) { + selectedLanguage.value = element; + } + } + } + } + }); + + isLoading.value = false; + } +} diff --git a/lib/controllers/chat_controller.dart b/lib/controllers/chat_controller.dart new file mode 100644 index 0000000..bf091d8 --- /dev/null +++ b/lib/controllers/chat_controller.dart @@ -0,0 +1,142 @@ +import 'dart:async'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../models/conversation_model.dart'; +import '../models/inbox_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:uuid/uuid.dart'; + +import '../service/send_notification.dart'; + +class ChatController extends GetxController { + Rx messageController = TextEditingController().obs; + + final ScrollController scrollController = ScrollController(); + + @override + void onInit() { + // TODO: implement onInit + if (scrollController.hasClients) { + Timer(const Duration(milliseconds: 500), () => scrollController.jumpTo(scrollController.position.maxScrollExtent)); + } + getArgument(); + super.onInit(); + } + + RxBool isLoading = true.obs; + RxString orderId = "".obs; + RxString customerId = "".obs; + RxString customerName = "".obs; + RxString customerProfileImage = "".obs; + RxString restaurantId = "".obs; + RxString restaurantName = "".obs; + RxString restaurantProfileImage = "".obs; + RxString token = "".obs; + RxString chatType = "".obs; + + void getArgument() { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + orderId.value = argumentData['orderId']; + customerId.value = argumentData['customerId']; + customerName.value = argumentData['customerName']; + customerProfileImage.value = argumentData['customerProfileImage'] ?? ""; + restaurantId.value = argumentData['restaurantId']; + restaurantName.value = argumentData['restaurantName']; + restaurantProfileImage.value = argumentData['restaurantProfileImage'] ?? ""; + token.value = argumentData['token'] ?? ""; + chatType.value = argumentData['chatType']; + } + isLoading.value = false; + } + + Future sendMessage(String message, Url? url, String videoThumbnail, String messageType) async { + InboxModel inboxModel = InboxModel( + lastSenderId: customerId.value, + customerId: customerId.value, + customerName: customerName.value, + restaurantId: restaurantId.value, + restaurantName: restaurantName.value, + createdAt: Timestamp.now(), + orderId: orderId.value, + customerProfileImage: customerProfileImage.value, + restaurantProfileImage: restaurantProfileImage.value, + lastMessage: messageController.value.text, + chatType: chatType.value, + ); + + print("chatType: ${chatType.value}"); + if (chatType.value == "Driver") { + await FireStoreUtils.addDriverInbox(inboxModel); + } else if (chatType.value == "worker" || chatType.value == "Worker") { + await FireStoreUtils.addWorkerInbox(inboxModel); + } else if (chatType.value == "provider" || chatType.value == "Provider") { + await FireStoreUtils.addProviderInbox(inboxModel); + } else { + await FireStoreUtils.addRestaurantInbox(inboxModel); + } + + ConversationModel conversationModel = ConversationModel( + id: const Uuid().v4(), + message: message, + senderId: customerId.value, + receiverId: restaurantId.value, + createdAt: Timestamp.now(), + url: url, + orderId: orderId.value, + messageType: messageType, + videoThumbnail: videoThumbnail, + ); + + if (url != null) { + if (url.mime.contains('image')) { + conversationModel.message = "sent a message".tr; + } else if (url.mime.contains('video')) { + conversationModel.message = "Sent a video".tr; + } else if (url.mime.contains('audio')) { + conversationModel.message = "Sent a audio".tr; + } + } + + if (chatType.value == "Driver") { + await FireStoreUtils.addDriverChat(conversationModel); + } else if (chatType.value == "worker" || chatType.value == "Worker") { + await FireStoreUtils.addWorkerChat(conversationModel); + } else if (chatType.value == "provider" || chatType.value == "Provider") { + await FireStoreUtils.addProviderChat(conversationModel); + } else { + await FireStoreUtils.addRestaurantChat(conversationModel); + } + + //await SendNotification.sendChatFcmMessage(customerName.value, conversationModel.message.toString(), token.value, {}); + await SendNotification.sendChatFcmMessage(customerName.value, conversationModel.message.toString(), token.value, { + "type": "chat", + "chatType": chatType.value, + "orderId": orderId.value, + "customerId": customerId.value, + "customerName": customerName.value, + "customerProfileImage": customerProfileImage.value, + "restaurantId": restaurantId.value, + "restaurantName": restaurantName.value, + "restaurantProfileImage": restaurantProfileImage.value, + "token": token.value, + }); + } + + final ImagePicker imagePicker = ImagePicker(); + + // Future pickFile({required ImageSource source}) async { + // try { + // XFile? image = await imagePicker.pickImage(source: source); + // if (image == null) return; + // Url url = await FireStoreUtils.uploadChatImageToFireStorage(File(image.path), Get.context!); + // sendMessage('', url, '', 'image'); + // Get.back(); + // } on PlatformException catch (e) { + // ShowToastDialog.showToast("${"failed_to_pick".tr} : \n $e"); + // } + // } +} diff --git a/lib/controllers/complain_controller.dart b/lib/controllers/complain_controller.dart new file mode 100644 index 0000000..4d22e89 --- /dev/null +++ b/lib/controllers/complain_controller.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../models/cab_order_model.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; + +class ComplainController extends GetxController { + Rx order = CabOrderModel().obs; + + final Rx title = TextEditingController().obs; + final Rx comment = TextEditingController().obs; + + final RxBool isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + + final args = Get.arguments; + if (args != null && args is Map && args['order'] is CabOrderModel) { + order.value = args['order'] as CabOrderModel; + getComplain(); + } else { + ShowToastDialog.showToast("Order data not found".tr); + Get.back(); + } + } + + Future getComplain() async { + isLoading.value = true; + try { + final data = await FireStoreUtils.getRideComplainData(order.value.id ?? ''); + if (data != null) { + title.value.text = data['title'] ?? ''; + comment.value.text = data['description'] ?? ''; + } + } catch (e) { + ShowToastDialog.showToast("Failed to load complaint".tr); + } finally { + isLoading.value = false; + } + } + + Future submitComplain() async { + // Validation + if (title.value.text.trim().isEmpty) { + ShowToastDialog.showToast("Please enter complaint title".tr); + return; + } + + if (comment.value.text.trim().isEmpty) { + ShowToastDialog.showToast("Please enter complaint description".tr); + return; + } + + isLoading.value = true; + ShowToastDialog.showLoader("Please wait..."); + + try { + // Check if complaint already exists + bool exists = await FireStoreUtils.isRideComplainAdded(order.value.id ?? ''); + + if (!exists) { + await FireStoreUtils.setRideComplain( + orderId: order.value.id ?? '', + title: title.value.text.trim(), + description: comment.value.text.trim(), + customerID: order.value.authorID ?? '', + customerName: "${order.value.author?.firstName ?? ''} ${order.value.author?.lastName ?? ''}".trim(), + driverID: order.value.driverId ?? '', + driverName: "${order.value.driver?.firstName ?? ''} ${order.value.driver?.lastName ?? ''}".trim(), + ); + + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Your complaint has been submitted to admin".tr); + Get.back(); + } else { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Your complaint is already submitted".tr); + } + } catch (e) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Something went wrong, please try again".tr); + } finally { + isLoading.value = false; + } + } + + @override + void onClose() { + title.value.dispose(); + comment.value.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/dash_board_controller.dart b/lib/controllers/dash_board_controller.dart new file mode 100644 index 0000000..ae6445f --- /dev/null +++ b/lib/controllers/dash_board_controller.dart @@ -0,0 +1,46 @@ +import 'package:customer/constant/constant.dart'; +import '../screen_ui/multi_vendor_service/favourite_screens/favourite_screen.dart'; +import '../screen_ui/multi_vendor_service/home_screen/home_screen.dart'; +import '../screen_ui/multi_vendor_service/home_screen/home_screen_two.dart'; +import '../screen_ui/multi_vendor_service/order_list_screen/order_screen.dart'; +import '../screen_ui/multi_vendor_service/profile_screen/profile_screen.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class DashBoardController extends GetxController { + RxInt selectedIndex = 0.obs; + + RxList pageList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getTaxList(); + if (Constant.sectionConstantModel!.theme == "theme_2") { + if (Constant.walletSetting == false) { + pageList.value = [const HomeScreen(), const FavouriteScreen(), const OrderScreen(), const ProfileScreen()]; + } else { + pageList.value = [const HomeScreen(), const FavouriteScreen(), const WalletScreen(), const OrderScreen(), const ProfileScreen()]; + } + } else { + if (Constant.walletSetting == false) { + pageList.value = [const HomeScreenTwo(), const FavouriteScreen(), const OrderScreen(), const ProfileScreen()]; + } else { + pageList.value = [const HomeScreenTwo(), const FavouriteScreen(), const WalletScreen(), const OrderScreen(), const ProfileScreen()]; + } + } + super.onInit(); + } + + Future getTaxList() async { + await FireStoreUtils.getTaxList(Constant.sectionConstantModel!.id).then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + } + + DateTime? currentBackPressTime; + RxBool canPopNow = false.obs; +} diff --git a/lib/controllers/dash_board_ecommarce_controller.dart b/lib/controllers/dash_board_ecommarce_controller.dart new file mode 100644 index 0000000..02bb9ce --- /dev/null +++ b/lib/controllers/dash_board_ecommarce_controller.dart @@ -0,0 +1,37 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/ecommarce/home_e_commerce_screen.dart'; +import '../screen_ui/multi_vendor_service/favourite_screens/favourite_screen.dart'; +import '../screen_ui/multi_vendor_service/order_list_screen/order_screen.dart'; +import '../screen_ui/multi_vendor_service/profile_screen/profile_screen.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class DashBoardEcommerceController extends GetxController { + RxInt selectedIndex = 0.obs; + + RxList pageList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getTaxList(); + if (Constant.walletSetting == false) { + pageList.value = [const HomeECommerceScreen(), const FavouriteScreen(), const OrderScreen(), const ProfileScreen()]; + } else { + pageList.value = [const HomeECommerceScreen(), const FavouriteScreen(), const WalletScreen(), const OrderScreen(), const ProfileScreen()]; + } + super.onInit(); + } + + Future getTaxList() async { + await FireStoreUtils.getTaxList(Constant.sectionConstantModel!.id).then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + } + + DateTime? currentBackPressTime; + RxBool canPopNow = false.obs; +} diff --git a/lib/controllers/dine_in_booking_controller.dart b/lib/controllers/dine_in_booking_controller.dart new file mode 100644 index 0000000..e273284 --- /dev/null +++ b/lib/controllers/dine_in_booking_controller.dart @@ -0,0 +1,34 @@ +import '../models/dine_in_booking_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class DineInBookingController extends GetxController { + RxBool isLoading = true.obs; + + RxBool isFeature = true.obs; + + RxList featureList = [].obs; + RxList historyList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getDineInBooking(); + super.onInit(); + } + + Future getDineInBooking() async { + await FireStoreUtils.getDineInBooking(true).then( + (value) { + featureList.value = value; + }, + ); + await FireStoreUtils.getDineInBooking(false).then( + (value) { + historyList.value = value; + }, + ); + + isLoading.value = false; + } +} diff --git a/lib/controllers/dine_in_booking_details_controller.dart b/lib/controllers/dine_in_booking_details_controller.dart new file mode 100644 index 0000000..994420c --- /dev/null +++ b/lib/controllers/dine_in_booking_details_controller.dart @@ -0,0 +1,24 @@ +import 'package:customer/models/dine_in_booking_model.dart'; +import 'package:get/get.dart'; + +class DineInBookingDetailsController extends GetxController { + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Rx bookingModel = DineInBookingModel().obs; + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + bookingModel.value = argumentData['bookingModel']; + } + isLoading.value = false; + update(); + } +} diff --git a/lib/controllers/dine_in_controller.dart b/lib/controllers/dine_in_controller.dart new file mode 100644 index 0000000..b582900 --- /dev/null +++ b/lib/controllers/dine_in_controller.dart @@ -0,0 +1,100 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import '../models/banner_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class DineInController extends GetxController { + RxBool isLoading = true.obs; + RxBool isPopular = true.obs; + + @override + void onInit() { + getCategory(); + getData(); + // TODO: implement onInit + super.onInit(); + } + + RxList vendorCategoryModel = [].obs; + + RxList allNearestRestaurant = [].obs; + RxList newArrivalRestaurantList = [].obs; + RxList popularRestaurantList = [].obs; + + RxList bannerBottomModel = [].obs; + Rx pageBottomController = PageController(viewportFraction: 0.877).obs; + RxInt currentBottomPage = 0.obs; + + RxList favouriteList = [].obs; + + Future getData() async { + isLoading.value = true; + await getZone(); + + FireStoreUtils.getAllNearestRestaurant(isDining: true).listen((event) async { + newArrivalRestaurantList.clear(); + allNearestRestaurant.clear(); + popularRestaurantList.clear(); + + allNearestRestaurant.addAll(event); + newArrivalRestaurantList.addAll(event); + popularRestaurantList.addAll(event); + + popularRestaurantList.sort( + (a, b) => Constant.calculateReview(reviewCount: b.reviewsCount.toString(), reviewSum: b.reviewsSum.toString()) + .compareTo(Constant.calculateReview(reviewCount: a.reviewsCount.toString(), reviewSum: a.reviewsSum.toString())), + ); + + newArrivalRestaurantList.sort( + (a, b) => (b.createdAt ?? Timestamp.now()).toDate().compareTo((a.createdAt ?? Timestamp.now()).toDate()), + ); + }); + + update(); + isLoading.value = false; + } + + Future getCategory() async { + await FireStoreUtils.getHomeVendorCategory().then( + (value) { + vendorCategoryModel.value = value; + }, + ); + + await FireStoreUtils.getHomeBottomBanner().then( + (value) { + bannerBottomModel.value = value; + }, + ); + if (Constant.userModel != null) { + await FireStoreUtils.getFavouriteRestaurant().then( + (value) { + favouriteList.value = value; + }, + ); + } + } + + Future getZone() async { + await FireStoreUtils.getZone().then((value) { + if (value != null) { + for (int i = 0; i < value.length; i++) { + if (Constant.isPointInPolygon(LatLng(Constant.selectedLocation.location!.latitude ?? 0.0, Constant.selectedLocation.location!.longitude ?? 0.0), value[i].area!)) { + Constant.selectedZone = value[i]; + Constant.isZoneAvailable = true; + break; + } else { + Constant.isZoneAvailable = false; + } + } + } + }); + } + +} diff --git a/lib/controllers/dine_in_restaurant_details_controller.dart b/lib/controllers/dine_in_restaurant_details_controller.dart new file mode 100644 index 0000000..478b3f8 --- /dev/null +++ b/lib/controllers/dine_in_restaurant_details_controller.dart @@ -0,0 +1,287 @@ +import 'dart:async'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/dine_in_booking_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_screen.dart'; +import '../service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +import '../service/send_notification.dart'; +import '../themes/show_toast_dialog.dart'; + +class DineInRestaurantDetailsController extends GetxController { + Rx searchEditingController = TextEditingController().obs; + + Rx additionRequestController = TextEditingController().obs; + + RxBool isLoading = true.obs; + RxBool firstVisit = false.obs; + Rx pageController = PageController().obs; + RxInt currentPage = 0.obs; + RxInt noOfQuantity = 1.obs; + + RxList favouriteList = [].obs; + RxList tags = [].obs; + + // List occasionList = ["Birthday", "Anniversary"]; + RxList occasionList = ["Birthday", "Anniversary"].obs; + + /// Localized title for each occasion + String getLocalizedOccasion(String key) { + switch (key) { + case "Birthday": + return "Birthday".tr; + case "Anniversary": + return "Anniversary".tr; + default: + return key; + } + } + RxString selectedOccasion = "".obs; + + RxList dateList = [].obs; + RxList timeSlotList = [].obs; + + Rx selectedDate = Timestamp.now().obs; + RxString selectedTimeSlot = '6:00 PM'.obs; + + RxString selectedTimeDiscount = '0'.obs; + RxString selectedTimeDiscountType = ''.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + getRecord(); + super.onInit(); + } + + Future orderBook() async { + ShowToastDialog.showLoader("Please wait...".tr); + + DateTime dt = selectedDate.value.toDate(); + String hour = DateFormat("kk:mm").format(DateFormat('hh:mm a').parse((Intl.getCurrentLocale() == "en_US") ? selectedTimeSlot.value : selectedTimeSlot.value.toLowerCase())); + dt = DateTime(dt.year, dt.month, dt.day, int.parse(hour.split(":")[0]), int.parse(hour.split(":")[1]), dt.second, dt.millisecond, dt.microsecond); + selectedDate.value = Timestamp.fromDate(dt); + DineInBookingModel dineInBookingModel = DineInBookingModel( + id: Constant.getUuid(), + author: Constant.userModel, + authorID: FireStoreUtils.getCurrentUid(), + createdAt: Timestamp.now(), + date: selectedDate.value, + status: Constant.orderPlaced, + vendor: vendorModel.value, + specialRequest: additionRequestController.value.text.isEmpty ? "" : additionRequestController.value.text, + vendorID: vendorModel.value.id, + guestEmail: Constant.userModel!.email, + guestFirstName: Constant.userModel!.firstName, + guestLastName: Constant.userModel!.lastName, + guestPhone: Constant.userModel!.phoneNumber, + occasion: selectedOccasion.value, + discount: selectedTimeDiscount.value, + discountType: selectedTimeDiscountType.value, + totalGuest: noOfQuantity.value.toString(), + firstVisit: firstVisit.value); + await FireStoreUtils.setBookedOrder(dineInBookingModel); + await SendNotification.sendFcmMessage(Constant.dineInPlaced, vendorModel.value.fcmToken.toString(), {}); + ShowToastDialog.closeLoader(); + Get.back(); + Get.to(const DineInBookingScreen()); + ShowToastDialog.showToast('Dine-In Request submitted successfully.'.tr); + } + + void getRecord() { + for (int i = 0; i < 7; i++) { + final now = DateTime.now().add(Duration(days: i)); + var day = DateFormat('EEEE').format(now); + if (vendorModel.value.specialDiscount?.isNotEmpty == true && vendorModel.value.specialDiscountEnable == true) { + for (var element in vendorModel.value.specialDiscount!) { + if (day == element.day.toString()) { + if (element.timeslot!.isNotEmpty) { + SpecialDiscountTimeslot employeeWithMaxSalary = + element.timeslot!.reduce((item1, item2) => double.parse(item1.discount.toString()) > double.parse(item2.discount.toString()) ? item1 : item2); + if (employeeWithMaxSalary.discountType == "dinein") { + DateModel model = DateModel(date: Timestamp.fromDate(now), discountPer: employeeWithMaxSalary.discount.toString()); + dateList.add(model); + } else { + DateModel model = DateModel(date: Timestamp.fromDate(now), discountPer: "0"); + dateList.add(model); + } + } else { + DateModel model = DateModel(date: Timestamp.fromDate(now), discountPer: "0"); + dateList.add(model); + } + } + } + } else { + DateModel model = DateModel(date: Timestamp.fromDate(now), discountPer: "0"); + dateList.add(model); + } + } + selectedDate.value = dateList.first.date; + + timeSet(selectedDate.value); + if (timeSlotList.isNotEmpty) { + selectedTimeSlot.value = DateFormat('hh:mm a').format(timeSlotList[0].time!); + } + } + + void timeSet(Timestamp selectedDate) { + timeSlotList.clear(); + + for (DateTime time = Constant.stringToDate(vendorModel.value.openDineTime.toString()); + time.isBefore(Constant.stringToDate(vendorModel.value.closeDineTime.toString())); + time = time.add(const Duration(minutes: 30))) { + final now = DateTime.parse(selectedDate.toDate().toString()); + var day = DateFormat('EEEE').format(now); + var date = DateFormat('dd-MM-yyyy').format(now); + + if (vendorModel.value.specialDiscount?.isNotEmpty == true && vendorModel.value.specialDiscountEnable == true) { + for (var element in vendorModel.value.specialDiscount!) { + if (day == element.day.toString()) { + if (element.timeslot!.isNotEmpty) { + for (var element in element.timeslot!) { + if (element.discountType == "dinein") { + var start = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.from}"); + var end = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.to}"); + var selected = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${DateFormat.Hm().format(time)}"); + + if (isCurrentDateInRangeDineIn(start, end, selected)) { + var contains = timeSlotList.where((element) => element.time == time); + if (contains.isNotEmpty) { + var index = timeSlotList.indexWhere((element) => element.time == time); + if (timeSlotList[index].discountPer == "0") { + timeSlotList.removeAt(index); + TimeModel model = TimeModel(time: time, discountPer: element.discount, discountType: element.type); + timeSlotList.insert(index == 0 ? 0 : index, model); + } + } else { + TimeModel model = TimeModel(time: time, discountPer: element.discount, discountType: element.type); + timeSlotList.add(model); + } + } else { + var contains = timeSlotList.where((element) => element.time == time); + if (contains.isEmpty) { + TimeModel model = TimeModel(time: time, discountPer: "0", discountType: "amount"); + timeSlotList.add(model); + } + } + } else { + TimeModel model = TimeModel(time: time, discountPer: "0", discountType: "amount"); + timeSlotList.add(model); + } + } + } else { + TimeModel model = TimeModel(time: time, discountPer: "0", discountType: "amount"); + timeSlotList.add(model); + } + } + } + } else { + TimeModel model = TimeModel(time: time, discountPer: "0", discountType: "amount"); + timeSlotList.add(model); + } + } + } + + void animateSlider() { + if (vendorModel.value.photos != null && vendorModel.value.photos!.isNotEmpty) { + Timer.periodic(const Duration(seconds: 2), (Timer timer) { + if (currentPage < vendorModel.value.photos!.length) { + currentPage++; + } else { + currentPage.value = 0; + } + + if (pageController.value.hasClients) { + pageController.value.animateToPage( + currentPage.value, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + }); + } + } + + Rx vendorModel = VendorModel().obs; + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorModel.value = argumentData['vendorModel']; + } + animateSlider(); + statusCheck(); + isLoading.value = false; + await getFavouriteList(); + + update(); + } + + Future getFavouriteList() async { + if (Constant.userModel != null) { + await FireStoreUtils.getFavouriteRestaurant().then( + (value) { + favouriteList.value = value; + }, + ); + } + + await FireStoreUtils.getVendorCuisines(vendorModel.value.id.toString()).then( + (value) { + tags.value = value; + }, + ); + update(); + } + + RxBool isOpen = false.obs; + + void statusCheck() { + final now = DateTime.now(); + var day = DateFormat('EEEE', 'en_US').format(now); + var date = DateFormat('dd-MM-yyyy').format(now); + for (var element in vendorModel.value.workingHours!) { + if (day == element.day.toString()) { + if (element.timeslot!.isNotEmpty) { + for (var element in element.timeslot!) { + var start = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.from}"); + var end = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.to}"); + if (isCurrentDateInRange(start, end)) { + isOpen.value = true; + } + } + } + } + } + } + + bool isCurrentDateInRangeDineIn(DateTime startDate, DateTime endDate, DateTime selected) { + return selected.isAtSameMomentAs(startDate) || selected.isAtSameMomentAs(endDate) || selected.isAfter(startDate) && selected.isBefore(endDate); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } +} + +class DateModel { + late Timestamp date; + late String discountPer; + + DateModel({required this.date, required this.discountPer}); +} + +class TimeModel { + DateTime? time; + String? discountPer; + String? discountType; + + TimeModel({required this.time, required this.discountPer, required this.discountType}); +} diff --git a/lib/controllers/discount_restaurant_list_controller.dart b/lib/controllers/discount_restaurant_list_controller.dart new file mode 100644 index 0000000..4b01083 --- /dev/null +++ b/lib/controllers/discount_restaurant_list_controller.dart @@ -0,0 +1,28 @@ +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:get/get.dart'; + +class DiscountRestaurantListController extends GetxController { + RxBool isLoading = true.obs; + RxList vendorList = [].obs; + RxList couponList = [].obs; + + RxString title = "Restaurants".obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorList.value = argumentData['vendorList']; + couponList.value = argumentData['couponList']; + title.value = argumentData['title'] ?? "Restaurants"; + } + isLoading.value = false; + } +} diff --git a/lib/controllers/edit_profile_controller.dart b/lib/controllers/edit_profile_controller.dart new file mode 100644 index 0000000..1208699 --- /dev/null +++ b/lib/controllers/edit_profile_controller.dart @@ -0,0 +1,77 @@ +import 'dart:io'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/user_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../themes/show_toast_dialog.dart'; + +class EditProfileController extends GetxController { + RxBool isLoading = true.obs; + Rx userModel = UserModel().obs; + + Rx firstNameController = TextEditingController().obs; + Rx lastNameController = TextEditingController().obs; + Rx emailController = TextEditingController().obs; + Rx phoneNumberController = TextEditingController().obs; + Rx countryCodeController = TextEditingController(text: "+91").obs; + + @override + void onInit() { + getData(); + super.onInit(); + } + + Future getData() async { + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) { + if (value != null) { + userModel.value = value; + firstNameController.value.text = userModel.value.firstName.toString(); + lastNameController.value.text = userModel.value.lastName.toString(); + emailController.value.text = userModel.value.email.toString(); + phoneNumberController.value.text = userModel.value.phoneNumber.toString(); + countryCodeController.value.text = userModel.value.countryCode.toString(); + profileImage.value = userModel.value.profilePictureURL ?? ""; + } + }); + + isLoading.value = false; + } + + Future saveData() async { + ShowToastDialog.showLoader("Please wait...".tr); + if (Constant().hasValidUrl(profileImage.value) == false && profileImage.value.isNotEmpty) { + profileImage.value = await Constant.uploadUserImageToFireStorage( + File(profileImage.value), + "profileImage/${FireStoreUtils.getCurrentUid()}", + File(profileImage.value).path.split('/').last, + ); + } + + userModel.value.firstName = firstNameController.value.text; + userModel.value.lastName = lastNameController.value.text; + userModel.value.profilePictureURL = profileImage.value; + + await FireStoreUtils.updateUser(userModel.value).then((value) { + ShowToastDialog.closeLoader(); + //Get.back(result: true); + }); + } + + final ImagePicker _imagePicker = ImagePicker(); + RxString profileImage = "".obs; + + Future pickFile({required ImageSource source}) async { + try { + XFile? image = await _imagePicker.pickImage(source: source); + if (image == null) return; + Get.back(); + profileImage.value = image.path; + } on PlatformException catch (e) { + ShowToastDialog.showToast("${"failed_to_pick".tr} : \n $e"); + } + } +} diff --git a/lib/controllers/enter_manually_location_controller.dart b/lib/controllers/enter_manually_location_controller.dart new file mode 100644 index 0000000..281b2e2 --- /dev/null +++ b/lib/controllers/enter_manually_location_controller.dart @@ -0,0 +1,83 @@ +import 'package:customer/service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../models/user_model.dart'; + +class EnterManuallyLocationController extends GetxController { + Rx userModel = UserModel().obs; + + RxList shippingAddressList = [].obs; + + List saveAsList = ['Home', 'Work', 'Hotel', 'other'].obs; + RxString selectedSaveAs = "Home".obs; + + Rx houseBuildingTextEditingController = TextEditingController().obs; + Rx localityEditingController = TextEditingController().obs; + Rx landmarkEditingController = TextEditingController().obs; + Rx location = UserLocation().obs; + Rx shippingModel = ShippingAddress().obs; + RxBool isLoading = false.obs; + RxBool isDefault = false.obs; + + RxString mode = "Add".obs; + + @override + void onInit() { + getArgument(); + super.onInit(); + } + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + //check mode + mode.value = argumentData['mode'] ?? "Add"; + + //check address + if (argumentData['address'] != null && argumentData['address'] is ShippingAddress) { + shippingModel.value = argumentData['address']; + setData(shippingModel.value); + } + } + + await getUser(); + isLoading.value = false; + update(); + } + + void setData(ShippingAddress shippingAddress) { + shippingModel.value = shippingAddress; + houseBuildingTextEditingController.value.text = shippingAddress.address.toString(); + localityEditingController.value.text = shippingAddress.locality.toString(); + landmarkEditingController.value.text = shippingAddress.landmark.toString(); + selectedSaveAs.value = shippingAddress.addressAs.toString(); + location.value = shippingAddress.location!; + } + + Future getUser() async { + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) { + if (value != null) { + userModel.value = value; + if (userModel.value.shippingAddress != null) { + shippingAddressList.value = userModel.value.shippingAddress!; + } + } + }); + } + + String getLocalizedSaveAs(String key) { + switch (key) { + case 'Home': + return 'Home'.tr; + case 'Work': + return 'Work'.tr; + case 'Hotel': + return 'Hotel'.tr; + case 'Other': + return 'Other'.tr; + default: + return key; + } + } +} diff --git a/lib/controllers/favourite_controller.dart b/lib/controllers/favourite_controller.dart new file mode 100644 index 0000000..149265e --- /dev/null +++ b/lib/controllers/favourite_controller.dart @@ -0,0 +1,93 @@ +import 'package:customer/constant/collection_name.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/favourite_item_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class FavouriteController extends GetxController { + RxBool favouriteRestaurant = true.obs; + RxList favouriteList = [].obs; + RxList favouriteVendorList = [].obs; + + RxList favouriteItemList = [].obs; + RxList favouriteFoodList = [].obs; + + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + Future getData() async { + if (Constant.userModel != null) { + await FireStoreUtils.getFavouriteRestaurant().then((value) { + favouriteList.value = value; + }); + + await FireStoreUtils.getFavouriteItem().then((value) { + favouriteItemList.value = value; + }); + + for (var element in favouriteList) { + await FireStoreUtils.getVendorById(element.restaurantId.toString()).then((value) async { + if (value != null) { + if ((Constant.isSubscriptionModelApplied == true || value.adminCommission?.isEnabled == true) && value.subscriptionPlan != null) { + if (value.subscriptionTotalOrders == "-1") { + favouriteVendorList.add(value); + } else { + print("Restaurant :: ${value.title.toString()}"); + if ((value.subscriptionExpiryDate != null && value.subscriptionExpiryDate!.toDate().isBefore(DateTime.now()) == false) || + value.subscriptionPlan?.expiryDay == '-1') { + if (value.subscriptionTotalOrders != '0') { + favouriteVendorList.add(value); + } + } + } + } else { + favouriteVendorList.add(value); + } + } + }); + } + + for (var element in favouriteItemList) { + await FireStoreUtils.getProductById(element.productId.toString()).then((value) async { + if (value != null) { + await FireStoreUtils.fireStore.collection(CollectionName.vendors).doc(value.vendorID.toString()).get().then((value1) async { + if (value1.exists) { + VendorModel vendorModel = VendorModel.fromJson(value1.data()!); + if(value.publish == true){ + if (Constant.isSubscriptionModelApplied == true || vendorModel.adminCommission?.isEnabled == true) { + if (vendorModel.subscriptionPlan != null) { + if (vendorModel.subscriptionTotalOrders == "-1") { + favouriteFoodList.add(value); + } else { + if ((vendorModel.subscriptionExpiryDate != null && vendorModel.subscriptionExpiryDate!.toDate().isBefore(DateTime.now()) == false) || + vendorModel.subscriptionPlan?.expiryDay == "-1") { + if (vendorModel.subscriptionTotalOrders != '0') { + favouriteFoodList.add(value); + } + } + } + } + } else { + favouriteFoodList.add(value); + } + } + + } + }); + } + }); + } + } + + isLoading.value = false; + } +} diff --git a/lib/controllers/favourite_ondemmand_controller.dart b/lib/controllers/favourite_ondemmand_controller.dart new file mode 100644 index 0000000..0ed8f26 --- /dev/null +++ b/lib/controllers/favourite_ondemmand_controller.dart @@ -0,0 +1,73 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/category_model.dart'; +import 'package:customer/models/favorite_ondemand_service_model.dart'; +import 'package:customer/models/provider_serivce_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class FavouriteOndemmandController extends GetxController { + // Add your controller logic here + + Rx isLoading = false.obs; + RxList lstFav = [].obs; + RxList categories = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + Future getData() async { + isLoading.value = true; + await FireStoreUtils.getOnDemandCategory().then((catValue) { + categories.value = catValue; + }); + await FireStoreUtils.getFavouritesServiceList(FireStoreUtils.getCurrentUid()).then((favList) { + lstFav.value = favList; + }); + isLoading.value = false; + } + + void toggleFavourite(ProviderServiceModel provider) { + if (Constant.userModel == null) { + Get.to(LoginScreen()); + } else { + var contain = lstFav.where((element) => element.service_id == provider.id); + if (contain.isNotEmpty) { + FavouriteOndemandServiceModel favouriteModel = FavouriteOndemandServiceModel( + section_id: provider.sectionId, + service_id: provider.id, + user_id: FireStoreUtils.getCurrentUid(), + serviceAuthorId: provider.author, + ); + FireStoreUtils.removeFavouriteOndemandService(favouriteModel); + lstFav.removeWhere((item) => item.service_id == provider.id); + } else { + FavouriteOndemandServiceModel favouriteModel = FavouriteOndemandServiceModel( + section_id: provider.sectionId, + service_id: provider.id, + user_id: FireStoreUtils.getCurrentUid(), + serviceAuthorId: provider.author, + ); + FireStoreUtils.setFavouriteOndemandSection(favouriteModel); + lstFav.add(favouriteModel); + } + } + } + + /// Get category by id safely from cached categories + Future getCategory(String? categoryId) async { + if (categoryId == null || categoryId.isEmpty) return null; + + // Try to find category from cached list + CategoryModel? cat = categories.firstWhereOrNull((element) => element.id == categoryId); + + // If not found, fetch from Firestore + cat ??= await FireStoreUtils.getCategoryById(categoryId); + + return cat; + } +} diff --git a/lib/controllers/food_home_controller.dart b/lib/controllers/food_home_controller.dart new file mode 100644 index 0000000..74f4b97 --- /dev/null +++ b/lib/controllers/food_home_controller.dart @@ -0,0 +1,191 @@ +import 'dart:developer'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dash_board_controller.dart'; +import 'package:customer/models/advertisement_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import '../models/banner_model.dart'; +import '../models/story_model.dart'; +import '../service/cart_provider.dart'; +import '../service/fire_store_utils.dart'; + +class FoodHomeController extends GetxController { + DashBoardController dashBoardController = Get.find(); + final CartProvider cartProvider = CartProvider(); + + Future getCartData() async { + cartProvider.cartStream.listen((event) async { + cartItem.clear(); + cartItem.addAll(event); + }); + update(); + } + + RxBool isLoading = true.obs; + RxBool isListView = true.obs; + RxBool isPopular = true.obs; + RxString selectedOrderTypeValue = "Delivery".tr.obs; + + Rx pageController = PageController(viewportFraction: 0.877).obs; + Rx pageBottomController = PageController(viewportFraction: 0.877).obs; + RxInt currentPage = 0.obs; + RxInt currentBottomPage = 0.obs; + + late TabController tabController; + + @override + void onInit() async { + await getData(); + super.onInit(); + } + + RxList vendorCategoryModel = [].obs; + + RxList allNearestRestaurant = [].obs; + RxList newArrivalRestaurantList = [].obs; + RxList advertisementList = [].obs; + RxList popularRestaurantList = [].obs; + RxList couponRestaurantList = [].obs; + RxList couponList = [].obs; + + RxList storyList = [].obs; + RxList bannerModel = [].obs; + RxList bannerBottomModel = [].obs; + + RxList favouriteList = [].obs; + + Future getData() async { + isLoading.value = true; + getCartData(); + selectedOrderTypeValue.value = Preferences.getString(Preferences.foodDeliveryType, defaultValue: "Delivery"); + await getZone(); + FireStoreUtils.getAllNearestRestaurant().listen((event) async { + popularRestaurantList.clear(); + newArrivalRestaurantList.clear(); + allNearestRestaurant.clear(); + advertisementList.clear(); + + allNearestRestaurant.addAll(event); + newArrivalRestaurantList.addAll(event); + popularRestaurantList.addAll(event); + Constant.restaurantList = allNearestRestaurant; + popularRestaurantList.sort( + (a, b) => Constant.calculateReview( + reviewCount: b.reviewsCount.toString(), + reviewSum: b.reviewsSum.toString(), + ).compareTo(Constant.calculateReview(reviewCount: a.reviewsCount.toString(), reviewSum: a.reviewsSum.toString())), + ); + + newArrivalRestaurantList.sort((a, b) => (b.createdAt ?? Timestamp.now()).toDate().compareTo((a.createdAt ?? Timestamp.now()).toDate())); + await getVendorCategory(); + await FireStoreUtils.getHomeCoupon().then((value) { + couponRestaurantList.clear(); + couponList.clear(); + for (var element1 in value) { + for (var element in allNearestRestaurant) { + if (element1.vendorID == element.id && element1.expiresAt!.toDate().isAfter(DateTime.now())) { + couponList.add(element1); + couponRestaurantList.add(element); + } + } + } + }); + + await FireStoreUtils.getStory().then((stories) { + storyList.clear(); + + print("Total stories fetched: ${stories.length}"); + // Create a fast lookup Set of all nearest vendor IDs + final nearestIds = allNearestRestaurant.map((e) => e.id).toSet(); + + print("nearestIds: $nearestIds"); + // Filter stories whose vendorID exists in nearestIds + storyList.addAll( + stories.where((story) => nearestIds.contains(story.vendorID)) + ); + print("Filtered storyList length: ${storyList.length}"); + }); + + if (Constant.isEnableAdsFeature == true) { + await FireStoreUtils.getAllAdvertisement().then((value) { + advertisementList.clear(); + for (var element1 in value) { + for (var element in allNearestRestaurant) { + if (element1.vendorId == element.id) { + advertisementList.add(element1); + } + } + } + }); + } + }); + setLoading(); + } + + Future setLoading() async { + await Future.delayed(Duration(seconds: 1), () async { + if (allNearestRestaurant.isEmpty) { + await Future.delayed(Duration(seconds: 2), () { + isLoading.value = false; + }); + } else { + isLoading.value = false; + } + update(); + }); + } + + Future getVendorCategory() async { + await FireStoreUtils.getHomeVendorCategory().then((value) { + vendorCategoryModel.value = value; + if (Constant.restaurantList != null) { + List usedCategoryIds = Constant.restaurantList!.expand((vendor) => vendor.categoryID ?? []).whereType().toSet().toList(); + vendorCategoryModel.value = vendorCategoryModel.where((category) => usedCategoryIds.contains(category.id)).toList(); + } + }); + + await FireStoreUtils.getHomeTopBanner().then((value) { + bannerModel.value = value; + }); + + await FireStoreUtils.getHomeBottomBanner().then((value) { + bannerBottomModel.value = value; + }); + + await getFavouriteRestaurant(); + } + + Future getFavouriteRestaurant() async { + if (Constant.userModel?.id != null) { + await FireStoreUtils.getFavouriteRestaurant().then((value) { + favouriteList.value = value; + }); + } + log("Constant.userModel?.id :: ${favouriteList.length}"); + } + + Future getZone() async { + await FireStoreUtils.getZone().then((value) { + if (value != null) { + for (int i = 0; i < value.length; i++) { + if (Constant.isPointInPolygon(LatLng(Constant.selectedLocation.location?.latitude ?? 0.0, Constant.selectedLocation.location?.longitude ?? 0.0), value[i].area!)) { + Constant.selectedZone = value[i]; + Constant.isZoneAvailable = true; + break; + } else { + Constant.selectedZone = value[i]; + Constant.isZoneAvailable = false; + } + } + } + }); + } +} diff --git a/lib/controllers/forgot_password_controller.dart b/lib/controllers/forgot_password_controller.dart new file mode 100644 index 0000000..3936731 --- /dev/null +++ b/lib/controllers/forgot_password_controller.dart @@ -0,0 +1,40 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../themes/show_toast_dialog.dart'; + +class ForgotPasswordController extends GetxController { + Rx emailEditingController = + TextEditingController().obs; + + Future forgotPassword() async { + final email = emailEditingController.value.text.trim(); + + if (email.isEmpty) { + ShowToastDialog.showToast("Please enter your email address.".tr); + return; + } + + if (!GetUtils.isEmail(email)) { + ShowToastDialog.showToast("Please enter a valid email address.".tr); + return; + } + + try { + ShowToastDialog.showLoader("Please wait...".tr); + await FirebaseAuth.instance.sendPasswordResetEmail(email: email); + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast( + 'reset_password_link_sent'.trParams({'email': email}), + ); + Get.back(); + } on FirebaseAuthException catch (e) { + ShowToastDialog.closeLoader(); + if (e.code == 'user-not-found') { + ShowToastDialog.showToast('No user found for that email.'.tr); + } else { + ShowToastDialog.showToast(e.message?.tr ?? "something_went_wrong".tr); + } + } + } +} diff --git a/lib/controllers/gift_card_controller.dart b/lib/controllers/gift_card_controller.dart new file mode 100644 index 0000000..3dd7565 --- /dev/null +++ b/lib/controllers/gift_card_controller.dart @@ -0,0 +1,920 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as maths; +import 'dart:math'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/gift_cards_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import '../models/gift_cards_order_model.dart'; +import '../models/payment_model/cod_setting_model.dart'; +import '../models/payment_model/flutter_wave_model.dart'; +import '../models/payment_model/mercado_pago_model.dart'; +import '../models/payment_model/mid_trans.dart'; +import '../models/payment_model/orange_money.dart'; +import '../models/payment_model/pay_fast_model.dart'; +import '../models/payment_model/pay_stack_model.dart'; +import '../models/payment_model/paypal_model.dart'; +import '../models/payment_model/paytm_model.dart'; +import '../models/payment_model/razorpay_model.dart'; +import '../models/payment_model/stripe_model.dart'; +import '../models/payment_model/wallet_setting_model.dart'; +import '../models/payment_model/xendit.dart'; +import '../models/user_model.dart'; +import '../models/wallet_transaction_model.dart'; +import '../payment/MercadoPagoScreen.dart'; +import '../payment/PayFastScreen.dart'; +import '../payment/getPaytmTxtToken.dart'; +import '../payment/midtrans_screen.dart'; +import '../payment/orangePayScreen.dart'; +import '../payment/paystack/pay_stack_screen.dart'; +import '../payment/paystack/pay_stack_url_model.dart'; +import '../payment/paystack/paystack_url_genrater.dart'; +import '../payment/stripe_failed_model.dart'; +import '../payment/xenditModel.dart'; +import '../payment/xenditScreen.dart'; +import '../screen_ui/multi_vendor_service/gift_card/history_gift_card.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../service/fire_store_utils.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; + +import '../themes/show_toast_dialog.dart'; + +class GiftCardController extends GetxController { + RxBool isLoading = true.obs; + RxString selectedPaymentMethod = ''.obs; + var pageController = PageController(); + + @override + void onInit() { + // TODO: implement onInit + getGiftCard(); + super.onInit(); + } + + List giftCardList = []; + Rx selectedGiftCard = GiftCardsModel().obs; + + List amountList = ["1000", "2000", "5000"]; + RxString selectedAmount = "1000".obs; + var selectedPageIndex = 0.obs; + + Rx amountController = TextEditingController().obs; + Rx messageController = TextEditingController().obs; + Rx userModel = UserModel().obs; + + Future getGiftCard() async { + await FireStoreUtils.getGiftCard().then((value) { + giftCardList = value; + if (giftCardList.isNotEmpty) { + selectedGiftCard.value = giftCardList.first; + messageController.value.text = selectedGiftCard.value.message.toString(); + } + }); + + isLoading.value = false; + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then( + (value) { + if (value != null) { + userModel.value = value; + } + }, + ); + await getPaymentSettings(); + } + + Future placeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + if (double.parse(userModel.value.walletAmount.toString()) >= double.parse(amountController.value.text)) { + setOrder(); + } else { + ShowToastDialog.showToast("You don't have sufficient wallet balance to purchase gift card".tr); + } + } else { + setOrder(); + } + } + + Future setOrder() async { + ShowToastDialog.showLoader("Please wait...".tr); + GiftCardsOrderModel giftCardsOrderModel = GiftCardsOrderModel(); + giftCardsOrderModel.id = const Uuid().v4(); + giftCardsOrderModel.giftId = selectedGiftCard.value.id.toString(); + giftCardsOrderModel.giftTitle = selectedGiftCard.value.title.toString(); + giftCardsOrderModel.price = amountController.value.text; + giftCardsOrderModel.redeem = false; + giftCardsOrderModel.message = messageController.value.text; + giftCardsOrderModel.giftPin = generateGiftPin(); + giftCardsOrderModel.giftCode = generateGiftCode(); + giftCardsOrderModel.paymentType = selectedPaymentMethod.value; + giftCardsOrderModel.createdDate = Timestamp.now(); + DateTime dateTime = DateTime.now().add(Duration(days: int.parse(selectedGiftCard.value.expiryDay ?? "2"))); + giftCardsOrderModel.expireDate = Timestamp.fromDate(dateTime); + giftCardsOrderModel.userid = FireStoreUtils.getCurrentUid(); + + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(amountController.value.text), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "user", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: giftCardsOrderModel.id, + note: "Gift card purchase amount debited".tr, + paymentStatus: "success".tr); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${amountController.value.text.toString()}", userId: FireStoreUtils.getCurrentUid()).then((value) {}); + } + }); + } + await FireStoreUtils.placeGiftCardOrder(giftCardsOrderModel); + ShowToastDialog.closeLoader(); + Get.off(const HistoryGiftCard()); + ShowToastDialog.showToast("Gift card Purchases successfully".tr); + } + + String generateGiftCode() { + var rng = Random(); + String generatedNumber = ''; + for (int i = 0; i < 16; i++) { + generatedNumber += (rng.nextInt(9) + 1).toString(); + } + return generatedNumber; + } + + String generateGiftPin() { + var rng = Random(); + String generatedNumber = ''; + for (int i = 0; i < 6; i++) { + generatedNumber += (rng.nextInt(9) + 1).toString(); + } + return generatedNumber; + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then( + (value) { + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + }else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'GoRide'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }, + ); + } + + // final _flutterPaypalNativePlugin = FlutterPaypalNative.instance; + + // void initPayPal() async { + // //set debugMode for error logging + // FlutterPaypalNative.isDebugMode = paytmModel.value.isSandboxEnabled == true ? true : false; + + // //initiate payPal plugin + // await _flutterPaypalNativePlugin.init( + // //your app id !!! No Underscore!!! see readme.md for help + // returnUrl: "com.parkme://paypalpay", + // //client id from developer dashboard + // clientID: payPalModel.value.paypalClient.toString(), + // //sandbox, staging, live etc + // payPalEnvironment: payPalModel.value.isLive == false ? FPayPalEnvironment.sandbox : FPayPalEnvironment.live, + // //what currency do you plan to use? default is US dollars + // currencyCode: FPayPalCurrencyCode.usd, + // //action paynow? + // action: FPayPalUserAction.payNow, + // ); + + // //call backs for payment + // _flutterPaypalNativePlugin.setPayPalOrderCallback( + // callback: FPayPalOrderCallback( + // onCancel: () { + // //user canceled the payment + // ShowToastDialog.showToast("Payment canceled"); + // }, + // onSuccess: (data) { + // //successfully paid + // //remove all items from queue + // // _flutterPaypalNativePlugin.removeAllPurchaseItems(); + // ShowToastDialog.showToast("Payment Successful!!"); + // placeOrder(); + // }, + // onError: (data) { + // //an error occured + // ShowToastDialog.showToast("error: ${data.reason}"); + // }, + // onShippingChange: (data) { + // //the user updated the shipping address + // ShowToastDialog.showToast("shipping change: ${data.shippingChangeAddress?.adminArea1 ?? ""}"); + // }, + // ), + // ); + // } + + // paypalPaymentSheet(String amount) { + // //add 1 item to cart. Max is 4! + // if (_flutterPaypalNativePlugin.canAddMorePurchaseUnit) { + // _flutterPaypalNativePlugin.addPurchaseUnit( + // FPayPalPurchaseUnit( + // // random prices + // amount: double.parse(amount), + + // ///please use your own algorithm for referenceId. Maybe ProductID? + // referenceId: FPayPalStrHelper.getRandomString(16), + // ), + // ); + // } + // // initPayPal(); + // _flutterPaypalNativePlugin.makeOrder( + // action: FPayPalUserAction.payNow, + // ); + // } + + // Strip + Future stripeMakePayment({required String amount}) async { + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin."); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay( + merchantCountryCode: 'US', + testEnv: true, + currencyCode: "USD", + ), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance( + colors: PaymentSheetAppearanceColors( + primary: AppThemeData.primary300, + ), + ), + merchantDisplayName: 'GoRide')); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": userModel.value.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post(Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}); + + return jsonDecode(response.body); + } catch (e) { + print(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = { + 'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', + 'Content-Type': 'application/json', + }; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + } + ], + "payer": {"email": userModel.value.email}, + "back_urls": { + "failure": "${Constant.globalUrl}payment/failure", + "pending": "${Constant.globalUrl}payment/pending", + "success": "${Constant.globalUrl}payment/success", + }, + "auto_return": "approved" // Automatically return after payment is approved + }); + + final response = await http.post( + Uri.parse("https://api.mercadopago.com/checkout/preferences"), + headers: headers, + body: body, + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen( + amount: (double.parse(totalAmount) * 100).toString(), currency: "ZAR", secretKey: payStackModel.value.secretKey.toString(), userModel: userModel.value) + .then((value) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to(PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ))! + .then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = { + 'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', + 'Content-Type': 'application/json', + }; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": userModel.value.email.toString(), + "phonenumber": userModel.value.phoneNumber, // Add a real phone number + "name": userModel.value.fullName(), // Add a real customer name + }, + "customizations": { + "title": "Payment for Services", + "description": "Payment for XYZ services", + } + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: userModel.value).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + +//PayPal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount} + }, + } + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + placeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }), + ), + ); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + + final response = await http.post( + Uri.parse( + getChecksum, + ), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID.toString(), + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), + }); + + final data = jsonDecode(response.body); + await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + initiatePayment(amount: amount, orderId: orderId).then((value) { + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + + GetPaymentTxtTokenModel result = value; + startTransaction(context, txnTokenBy: result.body.txnToken ?? '', orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + }); + }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // ShowToastDialog.showToast("Payment Successful!!"); + // placeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // print("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse( + getChecksum, + ), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID.toString(), + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), + "checksum_value": checkSum, + }); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post(Uri.parse(initiateURL), headers: {}, body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }); + print(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': { + 'contact': userModel.value.phoneNumber, + 'email': userModel.value.email, + }, + 'external': { + 'wallets': ['paytm'] + } + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen( + initialURl: url, + ))! + .then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!), + }, + body: jsonEncode({ + 'transaction_details': { + 'order_id': ordersId, + 'gross_amount': double.parse(amount.toString()).toInt(), + }, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to(() => OrangeMoneyScreen( + initialURl: paymentURL, + accessToken: accessToken, + amount: amount, + orangePay: orangeMoneyModel.value, + orderId: orderId, + payToken: payToken, + ))! + .then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + (); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = { + 'grant_type': 'client_credentials', + }; + + var response = await http.post(Uri.parse(apiUrl), + headers: { + 'Authorization': "Basic ${orangeMoneyModel.value.auth!}", + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + body: requestBody); + + // Handle the response + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + // ignore: use_build_context_synchronously + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = + orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen( + initialURl: model.invoiceUrl ?? '', + transId: model.id ?? '', + apiKey: xenditModel.value.apiKey!.toString(), + ))! + .then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/global_setting_controller.dart b/lib/controllers/global_setting_controller.dart new file mode 100644 index 0000000..1bcf70e --- /dev/null +++ b/lib/controllers/global_setting_controller.dart @@ -0,0 +1,48 @@ +import 'dart:developer'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/currency_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/utils/notification_service.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:get/get.dart'; +import '../constant/collection_name.dart'; +import '../service/fire_store_utils.dart'; + +class GlobalSettingController extends GetxController { + @override + void onInit() { + notificationInit(); + getCurrentCurrency(); + + super.onInit(); + } + + Future getCurrentCurrency() async { + FireStoreUtils.fireStore.collection(CollectionName.currencies).where("isActive", isEqualTo: true).snapshots().listen((event) { + if (event.docs.isNotEmpty) { + Constant.currencyModel = CurrencyModel.fromJson(event.docs.first.data()); + } else { + Constant.currencyModel = CurrencyModel(id: "", code: "USD", decimal: 2, isactive: true, name: "US Dollar", symbol: "\$", symbolatright: false); + } + }); + await FireStoreUtils.getSettings(); + } + + NotificationService notificationService = NotificationService(); + + void notificationInit() { + notificationService.initInfo().then((value) async { + String token = await NotificationService.getToken(); + log(":::::::TOKEN:::::: $token"); + if (FirebaseAuth.instance.currentUser != null) { + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) { + if (value != null) { + UserModel driverUserModel = value; + driverUserModel.fcmToken = token; + FireStoreUtils.updateUser(driverUserModel); + } + }); + } + }); + } +} diff --git a/lib/controllers/history_gift_card_controller.dart b/lib/controllers/history_gift_card_controller.dart new file mode 100644 index 0000000..aae9f46 --- /dev/null +++ b/lib/controllers/history_gift_card_controller.dart @@ -0,0 +1,38 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/gift_cards_order_model.dart'; +import 'package:share_plus/share_plus.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class HistoryGiftCardController extends GetxController { + RxList giftCardsOrderList = [].obs; + RxBool isLoading = true.obs; + + @override + void onInit() { + getData(); + super.onInit(); + } + + Future getData() async { + await FireStoreUtils.getGiftHistory().then((value) { + giftCardsOrderList.value = value; + }); + isLoading.value = false; + } + + void updateList(int index) { + GiftCardsOrderModel giftCardsOrderModel = giftCardsOrderList[index]; + giftCardsOrderModel.isPasswordShow = giftCardsOrderModel.isPasswordShow == true ? false : true; + + giftCardsOrderList.removeAt(index); + giftCardsOrderList.insert(index, giftCardsOrderModel); + } + + Future share(String giftCode, String giftPin, String msg, String amount, Timestamp date) async { + await Share.share( + "${'Gift Code :'.tr} $giftCode\n${'Gift Pin :'.tr} $giftPin\n${'Price :'.tr} ${Constant.amountShow(amount: amount)}\n${'Expire Date :'.tr} ${date.toDate()}\n\n${'Message'.tr} : $msg", + ); + } +} diff --git a/lib/controllers/home_e_commerce_controller.dart b/lib/controllers/home_e_commerce_controller.dart new file mode 100644 index 0000000..d92daa0 --- /dev/null +++ b/lib/controllers/home_e_commerce_controller.dart @@ -0,0 +1,132 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/advertisement_model.dart'; +import 'package:customer/models/banner_model.dart'; +import 'package:customer/models/brands_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/service/cart_provider.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../constant/constant.dart'; + +class HomeECommerceController extends GetxController { + final CartProvider cartProvider = CartProvider(); + + Future getCartData() async { + cartProvider.cartStream.listen((event) async { + cartItem.clear(); + cartItem.addAll(event); + }); + update(); + } + + RxBool isLoading = true.obs; + RxBool isListView = true.obs; + RxBool isPopular = true.obs; + + Rx pageController = PageController(viewportFraction: 0.877).obs; + Rx pageBottomController = PageController(viewportFraction: 0.877).obs; + RxInt currentPage = 0.obs; + RxInt currentBottomPage = 0.obs; + + @override + void onInit() { + // TODO: implement onInit + getVendorCategory(); + getData(); + super.onInit(); + } + + RxList vendorCategoryModel = [].obs; + RxList categoryWiseProductList = [].obs; + + RxList allNearestRestaurant = [].obs; + RxList newArrivalRestaurantList = [].obs; + RxList advertisementList = [].obs; + + RxList bannerModel = [].obs; + RxList bannerBottomModel = [].obs; + RxList brandList = [].obs; + + Future getData() async { + isLoading.value = true; + getCartData(); + FireStoreUtils.getAllNearestRestaurant().listen((event) async { + print("=====>${event.length}"); + + newArrivalRestaurantList.clear(); + allNearestRestaurant.clear(); + advertisementList.clear(); + + allNearestRestaurant.addAll(event); + newArrivalRestaurantList.addAll(event); + Constant.restaurantList = allNearestRestaurant; + List usedCategoryIds = allNearestRestaurant.expand((vendor) => vendor.categoryID ?? []).whereType().toSet().toList(); + vendorCategoryModel.value = vendorCategoryModel.where((category) => usedCategoryIds.contains(category.id)).toList(); + + newArrivalRestaurantList.sort((a, b) => (b.createdAt ?? Timestamp.now()).toDate().compareTo((a.createdAt ?? Timestamp.now()).toDate())); + + if (Constant.isEnableAdsFeature == true) { + await FireStoreUtils.getAllAdvertisement().then((value) { + advertisementList.clear(); + for (var element1 in value) { + for (var element in allNearestRestaurant) { + if (element1.vendorId == element.id) { + advertisementList.add(element1); + } + } + } + }); + } + }); + setLoading(); + } + + Future setLoading() async { + await Future.delayed(Duration(seconds: 1), () async { + if (allNearestRestaurant.isEmpty) { + await Future.delayed(Duration(seconds: 2), () { + isLoading.value = false; + }); + } else { + isLoading.value = false; + } + update(); + }); + } + + Future getVendorCategory() async { + await FireStoreUtils.getHomeVendorCategory().then((value) { + vendorCategoryModel.value = value; + }); + await FireStoreUtils.getHomePageShowCategory().then((value) { + categoryWiseProductList.value = value; + }); + + await FireStoreUtils.getHomeTopBanner().then((value) { + bannerModel.value = value; + }); + + await FireStoreUtils.getHomeBottomBanner().then((value) { + bannerBottomModel.value = value; + }); + + await FireStoreUtils.getBrandList().then((value) { + brandList.value = value; + }); + await getFavouriteRestaurant(); + } + + RxList favouriteList = [].obs; + + Future getFavouriteRestaurant() async { + if (Constant.userModel?.id != null) { + await FireStoreUtils.getFavouriteRestaurant().then((value) { + favouriteList.value = value; + }); + } + } +} diff --git a/lib/controllers/home_parcel_controller.dart b/lib/controllers/home_parcel_controller.dart new file mode 100644 index 0000000..aaccf75 --- /dev/null +++ b/lib/controllers/home_parcel_controller.dart @@ -0,0 +1,42 @@ +import 'dart:developer'; + +import 'package:get/get.dart'; +import '../models/banner_model.dart'; +import '../models/parcel_category.dart'; +import '../service/fire_store_utils.dart'; + +class HomeParcelController extends GetxController { + RxBool isLoading = true.obs; + + RxList bannerTopHome = [].obs; + RxList parcelCategory = [].obs; + + @override + void onInit() { + super.onInit(); + loadData(); + } + + void loadData() async { + try { + isLoading.value = true; + + // Load banners + await FireStoreUtils.getHomeTopBanner().then((value) { + bannerTopHome.value = value; + log('Banners loaded: ${bannerTopHome.length}'); + }); + + // Load parcel categories + await FireStoreUtils.getParcelServiceCategory().then((value) { + parcelCategory.value = value; + log('Parcel categories loaded: ${parcelCategory.length}'); + }); + } catch (e) { + bannerTopHome.clear(); + parcelCategory.clear(); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/controllers/live_tracking_controller.dart b/lib/controllers/live_tracking_controller.dart new file mode 100644 index 0000000..dbc245c --- /dev/null +++ b/lib/controllers/live_tracking_controller.dart @@ -0,0 +1,256 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:customer/constant/collection_name.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/order_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:latlong2/latlong.dart' as location; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:http/http.dart' as http; + +class LiveTrackingController extends GetxController { + GoogleMapController? mapController; + final flutterMap.MapController osmMapController = flutterMap.MapController(); + + Rx orderModel = OrderModel().obs; + Rx driverUserModel = UserModel().obs; + RxBool isLoading = true.obs; + + Rx source = location.LatLng(0, 0).obs; + Rx destination = location.LatLng(0, 0).obs; + Rx driverCurrent = location.LatLng(0, 0).obs; + + RxList routePoints = [].obs; + RxMap markers = {}.obs; + RxMap polyLines = {}.obs; + RxList osmMarkers = [].obs; + + BitmapDescriptor? pickupIcon; + BitmapDescriptor? dropoffIcon; + BitmapDescriptor? driverIcon; + + PolylinePoints polylinePoints = PolylinePoints(apiKey: Constant.mapAPIKey); + + StreamSubscription? orderSub; + StreamSubscription? driverSub; + + @override + void onInit() { + super.onInit(); + addMarkerIcons(); + getArguments(); + } + + @override + void onClose() { + orderSub?.cancel(); + driverSub?.cancel(); + super.onClose(); + } + + Future getArguments() async { + final args = Get.arguments; + if (args == null) return; + + orderModel.value = args['orderModel']; + + orderSub = FireStoreUtils.fireStore.collection(CollectionName.vendorOrders).doc(orderModel.value.id).snapshots().listen((orderSnap) { + if (orderSnap.data() == null) return; + orderModel.value = OrderModel.fromJson(orderSnap.data()!); + + if (orderModel.value.driverID != null) { + driverSub?.cancel(); + driverSub = FireStoreUtils.fireStore.collection(CollectionName.users).doc(orderModel.value.driverID).snapshots().listen((driverSnap) async { + if (driverSnap.data() == null) return; + driverUserModel.value = UserModel.fromJson(driverSnap.data()!); + await updateLiveTracking(); + }); + } + + if (orderModel.value.status == Constant.orderCompleted) { + Get.back(); + } + }); + + isLoading.value = false; + } + + Future updateLiveTracking() async { + driverCurrent.value = location.LatLng(driverUserModel.value.location?.latitude ?? 0.0, driverUserModel.value.location?.longitude ?? 0.0); + + source.value = location.LatLng(orderModel.value.vendor?.latitude ?? 0.0, orderModel.value.vendor?.longitude ?? 0.0); + + destination.value = location.LatLng(orderModel.value.address?.location?.latitude ?? 0.0, orderModel.value.address?.location?.longitude ?? 0.0); + + if (orderModel.value.status == Constant.orderPlaced || orderModel.value.status == Constant.orderAccepted) { + await showDriverToRestaurantRoute(); + } else if (orderModel.value.status == Constant.orderShipped || orderModel.value.status == Constant.orderInTransit) { + await showDriverToCustomerRoute(); + } + } + + Future showDriverToRestaurantRoute() async { + clearOldData(); + if (Constant.selectedMapType == 'osm') { + await fetchRoute(driverCurrent.value, source.value); + addOsmMarkers(showPickup: true, showDrop: false); + animateToOSMLocation(driverCurrent.value); + } else { + await getPolyline( + sourceLatitude: driverCurrent.value.latitude, + sourceLongitude: driverCurrent.value.longitude, + destinationLatitude: source.value.latitude, + destinationLongitude: source.value.longitude, + showPickup: true, + showDrop: false, + ); + } + } + + Future showDriverToCustomerRoute() async { + clearOldData(); + if (Constant.selectedMapType == 'osm') { + await fetchRoute(driverCurrent.value, destination.value); + addOsmMarkers(showPickup: false, showDrop: true); + animateToOSMLocation(driverCurrent.value); + } else { + await getPolyline( + sourceLatitude: driverCurrent.value.latitude, + sourceLongitude: driverCurrent.value.longitude, + destinationLatitude: destination.value.latitude, + destinationLongitude: destination.value.longitude, + showPickup: false, + showDrop: true, + ); + } + } + + void clearOldData() { + markers.clear(); + polyLines.clear(); + routePoints.clear(); + } + + Future fetchRoute(location.LatLng source, location.LatLng destination) async { + final url = Uri.parse( + 'https://router.project-osrm.org/route/v1/driving/${source.longitude},${source.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=geojson', + ); + final response = await http.get(url); + if (response.statusCode == 200) { + final data = json.decode(response.body); + final coords = data['routes'][0]['geometry']['coordinates']; + routePoints.value = coords.map((c) => location.LatLng(c[1].toDouble(), c[0].toDouble())).toList(); + } + } + + void animateToOSMLocation(location.LatLng loc) { + osmMapController.move(loc, 15); + } + + void addOsmMarkers({bool showPickup = false, bool showDrop = false}) { + final List tempMarkers = [ + // Driver Marker + flutterMap.Marker(point: driverCurrent.value, width: 40, height: 40, child: Image.asset('assets/images/food_delivery.png')), + ]; + + if (showPickup) { + tempMarkers.add(flutterMap.Marker(point: source.value, width: 40, height: 40, child: Image.asset('assets/images/pickup.png'))); + } + + if (showDrop) { + tempMarkers.add(flutterMap.Marker(point: destination.value, width: 40, height: 40, child: Image.asset('assets/images/dropoff.png'))); + } + + osmMarkers.value = tempMarkers; + } + + Future getPolyline({ + required double sourceLatitude, + required double sourceLongitude, + required double destinationLatitude, + required double destinationLongitude, + bool showPickup = false, + bool showDrop = false, + }) async { + List polylineCoordinates = []; + + PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( + request: PolylineRequest(origin: PointLatLng(sourceLatitude, sourceLongitude), destination: PointLatLng(destinationLatitude, destinationLongitude), mode: TravelMode.driving), + ); + + if (result.points.isNotEmpty) { + polylineCoordinates = result.points.map((e) => LatLng(e.latitude, e.longitude)).toList(); + } + + addGoogleMarkers(showPickup: showPickup, showDrop: showDrop); + _addPolyLine(polylineCoordinates); + } + + void addGoogleMarkers({bool showPickup = false, bool showDrop = false}) { + markers.clear(); + + // Always show driver marker + if (driverUserModel.value.location != null && driverIcon != null) { + addMarker( + id: "Driver", + latitude: driverUserModel.value.location?.latitude ?? 0.0, + longitude: driverUserModel.value.location?.longitude ?? 0.0, + descriptor: driverIcon!, + rotation: (driverUserModel.value.rotation ?? 0).toDouble(), + ); + } + + if (showPickup && orderModel.value.vendor?.latitude != null && pickupIcon != null) { + addMarker(id: "Pickup", latitude: orderModel.value.vendor!.latitude ?? 0.0, longitude: orderModel.value.vendor!.longitude ?? 0.0, descriptor: pickupIcon!, rotation: 0.0); + } else if (showDrop && orderModel.value.address?.location?.latitude != null && dropoffIcon != null) { + addMarker( + id: "Drop", + latitude: orderModel.value.address!.location!.latitude ?? 0.0, + longitude: orderModel.value.address!.location!.longitude ?? 0.0, + descriptor: dropoffIcon!, + rotation: 0.0, + ); + } + } + + void addMarker({required String id, required double latitude, required double longitude, required BitmapDescriptor descriptor, required double rotation}) { + MarkerId markerId = MarkerId(id); + markers[markerId] = Marker(markerId: markerId, icon: descriptor, position: LatLng(latitude, longitude), rotation: rotation, anchor: const Offset(0.5, 0.5)); + } + + Future addMarkerIcons() async { + if (Constant.selectedMapType == 'osm') return; + + pickupIcon = BitmapDescriptor.fromBytes(await Constant().getBytesFromAsset('assets/images/pickup.png', 100)); + dropoffIcon = BitmapDescriptor.fromBytes(await Constant().getBytesFromAsset('assets/images/dropoff.png', 100)); + driverIcon = BitmapDescriptor.fromBytes(await Constant().getBytesFromAsset('assets/images/food_delivery.png', 100)); + } + + Future _addPolyLine(List polylineCoordinates) async { + if (polylineCoordinates.isEmpty) return; + + PolylineId id = const PolylineId("poly"); + Polyline polyline = Polyline(polylineId: id, color: Colors.blue, width: 5, points: polylineCoordinates); + + polyLines[id] = polyline; + await updateCameraBounds(polylineCoordinates); + } + + Future updateCameraBounds(List points) async { + if (mapController == null || points.isEmpty) return; + + double minLat = points.map((e) => e.latitude).reduce((a, b) => a < b ? a : b); + double maxLat = points.map((e) => e.latitude).reduce((a, b) => a > b ? a : b); + double minLng = points.map((e) => e.longitude).reduce((a, b) => a < b ? a : b); + double maxLng = points.map((e) => e.longitude).reduce((a, b) => a > b ? a : b); + + LatLngBounds bounds = LatLngBounds(southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng)); + + await mapController!.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80)); + } +} diff --git a/lib/controllers/login_controller.dart b/lib/controllers/login_controller.dart new file mode 100644 index 0000000..fd729ad --- /dev/null +++ b/lib/controllers/login_controller.dart @@ -0,0 +1,273 @@ +import 'dart:convert'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; +import '../constant/constant.dart'; +import '../models/user_model.dart'; +import '../screen_ui/auth_screens/login_screen.dart'; +import '../screen_ui/auth_screens/sign_up_screen.dart'; +import '../screen_ui/service_home_screen/service_list_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; +import '../utils/notification_service.dart'; +import 'package:crypto/crypto.dart'; + +class LoginController extends GetxController { + Rx emailController = TextEditingController().obs; + Rx passwordController = TextEditingController().obs; + + /// Focus nodes + final FocusNode emailFocusNode = FocusNode(); + final FocusNode passwordFocusNode = FocusNode(); + + /// Loading indicator + final RxBool isLoading = false.obs; + + RxBool passwordVisible = true.obs; + + Future loginWithEmail() async { + final email = emailController.value.text.trim(); + final password = passwordController.value.text.trim(); + if (email.isEmpty || !email.contains('@')) { + ShowToastDialog.showToast("Please enter a valid email address".tr); + return; + } + + if (password.isEmpty) { + ShowToastDialog.showToast("Please enter your password".tr); + return; + } + + try { + isLoading.value = true; + ShowToastDialog.showLoader("Logging in...".tr); + + final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password); + + final userModel = await FireStoreUtils.getUserProfile(credential.user!.uid); + + if (userModel != null && userModel.role == Constant.userRoleCustomer) { + if (userModel.active == true) { + userModel.fcmToken = await NotificationService.getToken(); + await FireStoreUtils.updateUser(userModel); + + if (userModel.shippingAddress != null && userModel.shippingAddress!.isNotEmpty) { + final defaultAddress = userModel.shippingAddress!.firstWhere( + (e) => e.isDefault == true, + orElse: () => userModel.shippingAddress!.first, + ); + + Constant.selectedLocation = defaultAddress; + + Get.offAll(() => const ServiceListScreen()); + } else { + Get.offAll(() => const LocationPermissionScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + ShowToastDialog.showToast("This user is disabled. Please contact admin.".tr); + Get.offAll(() => const LoginScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + ShowToastDialog.showToast("This user does not exist in the customer app.".tr); + Get.offAll(() => const LoginScreen()); + } + } on FirebaseAuthException catch (e) { + if (e.code == 'user-not-found') { + ShowToastDialog.showToast("No user found for that email.".tr); + } else if (e.code == 'wrong-password') { + ShowToastDialog.showToast("Wrong password provided.".tr); + } else if (e.code == 'invalid-email') { + ShowToastDialog.showToast("Invalid email.".tr); + } else { + ShowToastDialog.showToast(e.message?.tr ?? "Login failed. Please try again.".tr); + } + } finally { + isLoading.value = false; + ShowToastDialog.closeLoader(); + } + } + + Future loginWithGoogle() async { + ShowToastDialog.showLoader("please wait...".tr); + await signInWithGoogle().then((value) async { + ShowToastDialog.closeLoader(); + if (value != null) { + if (value.additionalUserInfo!.isNewUser) { + UserModel userModel = UserModel(); + userModel.id = value.user!.uid; + userModel.email = value.user!.email; + userModel.firstName = value.user!.displayName?.split(' ').first; + userModel.lastName = value.user!.displayName?.split(' ').last; + userModel.provider = 'google'; + + ShowToastDialog.closeLoader(); + Get.off(const SignUpScreen(), arguments: {"userModel": userModel, "type": "google"}); + } else { + await FireStoreUtils.userExistOrNot(value.user!.uid).then((userExit) async { + ShowToastDialog.closeLoader(); + if (userExit == true) { + UserModel? userModel = await FireStoreUtils.getUserProfile(value.user!.uid); + if (userModel != null && userModel.role == Constant.userRoleCustomer) { + if (userModel.active == true) { + userModel.fcmToken = await NotificationService.getToken(); + await FireStoreUtils.updateUser(userModel); + + if (userModel.shippingAddress != null && userModel.shippingAddress!.isNotEmpty) { + final defaultAddress = userModel.shippingAddress!.firstWhere( + (e) => e.isDefault == true, + orElse: () => userModel.shippingAddress!.first, + ); + + Constant.selectedLocation = defaultAddress; + + Get.offAll(() => const ServiceListScreen()); + } else { + Get.offAll(() => const LocationPermissionScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + ShowToastDialog.showToast("This user is disabled. Please contact admin.".tr); + Get.offAll(() => const LoginScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + ShowToastDialog.showToast("This user does not exist in the customer app.".tr); + Get.offAll(() => const LoginScreen()); + } + } else { + UserModel userModel = UserModel(); + userModel.id = value.user!.uid; + userModel.email = value.user!.email; + userModel.firstName = value.user!.displayName?.split(' ').first; + userModel.lastName = value.user!.displayName?.split(' ').last; + userModel.provider = 'google'; + + Get.off(const SignUpScreen(), arguments: {"userModel": userModel, "type": "google"}); + } + }); + } + } + }); + } + + Future loginWithApple() async { + ShowToastDialog.showLoader("please wait...".tr); + await signInWithApple().then((value) async { + ShowToastDialog.closeLoader(); + if (value != null) { + Map map = value; + AuthorizationCredentialAppleID appleCredential = map['appleCredential']; + UserCredential userCredential = map['userCredential']; + + if (userCredential.additionalUserInfo!.isNewUser) { + // New user → go to sign-up + UserModel userModel = UserModel(); + userModel.id = userCredential.user!.uid; + userModel.email = appleCredential.email; + userModel.firstName = appleCredential.givenName; + userModel.lastName = appleCredential.familyName; + userModel.provider = 'apple'; + + Get.off(const SignUpScreen(), arguments: {"userModel": userModel, "type": "apple"}); + } else { + // Existing user + await FireStoreUtils.userExistOrNot(userCredential.user!.uid).then((userExit) async { + if (userExit == true) { + UserModel? userModel = await FireStoreUtils.getUserProfile(userCredential.user!.uid); + if (userModel != null && userModel.role == Constant.userRoleCustomer) { + if (userModel.active == true) { + userModel.fcmToken = await NotificationService.getToken(); + await FireStoreUtils.updateUser(userModel); + + if (userModel.shippingAddress != null && userModel.shippingAddress!.isNotEmpty) { + final defaultAddress = userModel.shippingAddress!.firstWhere( + (e) => e.isDefault == true, + orElse: () => userModel.shippingAddress!.first, + ); + + Constant.selectedLocation = defaultAddress; + Get.offAll(() => const ServiceListScreen()); + } else { + Get.offAll(() => const LocationPermissionScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + ShowToastDialog.showToast("This user is disabled. Please contact admin.".tr); + Get.offAll(() => const LoginScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + ShowToastDialog.showToast("This user does not exist in the customer app.".tr); + Get.offAll(() => const LoginScreen()); + } + } else { + // User not in DB → go to signup + UserModel userModel = UserModel(); + userModel.id = userCredential.user!.uid; + userModel.email = appleCredential.email; + userModel.firstName = appleCredential.givenName; + userModel.lastName = appleCredential.familyName; + userModel.provider = 'apple'; + + Get.off(const SignUpScreen(), arguments: {"userModel": userModel, "type": "apple"}); + } + }); + } + } + }); + } + + Future signInWithGoogle() async { + try { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + await googleSignIn.initialize(); + + final GoogleSignInAccount googleUser = await googleSignIn.authenticate(); + if (googleUser.id.isEmpty) return null; + + final GoogleSignInAuthentication googleAuth = googleUser.authentication; + + final credential = GoogleAuthProvider.credential(idToken: googleAuth.idToken); + final userCredential = await FirebaseAuth.instance.signInWithCredential(credential); + + return userCredential; + } catch (e) { + print("Google Sign-In Error: $e"); + return null; + } + } + + String sha256ofString(String input) { + final bytes = utf8.encode(input); + final digest = sha256.convert(bytes); + return digest.toString(); + } + + Future?> signInWithApple() async { + try { + final rawNonce = generateNonce(); + final nonce = sha256ofString(rawNonce); + + AuthorizationCredentialAppleID appleCredential = await SignInWithApple.getAppleIDCredential( + scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName], + nonce: nonce, + ); + + final oauthCredential = OAuthProvider( + "apple.com", + ).credential(idToken: appleCredential.identityToken, rawNonce: rawNonce, accessToken: appleCredential.authorizationCode); + + UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(oauthCredential); + return {"appleCredential": appleCredential, "userCredential": userCredential}; + } catch (e) { + debugPrint(e.toString()); + } + return null; + } +} diff --git a/lib/controllers/map_view_controller.dart b/lib/controllers/map_view_controller.dart new file mode 100644 index 0000000..a7e148e --- /dev/null +++ b/lib/controllers/map_view_controller.dart @@ -0,0 +1,110 @@ +import 'dart:typed_data'; +import 'package:customer/constant/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:latlong2/latlong.dart' as location; +import '../screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart'; +import 'food_home_controller.dart'; + +class MapViewController extends GetxController { + GoogleMapController? mapController; + BitmapDescriptor? parkingMarker; + BitmapDescriptor? currentLocationMarker; + + FoodHomeController homeController = Get.find(); + Image? departureOsmIcon; //OSM + + RxList osmMarker = [].obs; + final flutterMap.MapController osmMapController = flutterMap.MapController(); + + @override + void onInit() { + // TODO: implement onInit + addMarkerSetup(); + super.onInit(); + } + + Future addMarkerSetup() async { + if (Constant.selectedMapType == "osm") { + departureOsmIcon = Image.asset( + "assets/images/map_selected.png", + width: 30, + height: 30, + ); //OSM + + for (var element in homeController.allNearestRestaurant) { + osmMarker.add( + flutterMap.Marker( + point: location.LatLng( + element.latitude ?? 0.0, + element.longitude ?? 0.0, + ), + width: 40, + height: 40, + child: GestureDetector( + onTap: () { + Get.to( + RestaurantDetailsScreen(), + arguments: {"vendorModel": element}, + ); + }, + child: departureOsmIcon, + ), + ), + ); + } + } else { + final Uint8List parking = await Constant().getBytesFromAsset( + "assets/images/map_selected.png", + 20, + ); + parkingMarker = BitmapDescriptor.bytes(parking); + for (var element in homeController.allNearestRestaurant) { + addMarker( + latitude: element.latitude, + longitude: element.longitude, + id: element.id.toString(), + rotation: 0, + descriptor: parkingMarker!, + title: element.title.toString(), + ); + } + } + } + + RxMap markers = {}.obs; + + void addMarker({ + required double? latitude, + required double? longitude, + required String id, + required BitmapDescriptor descriptor, + required double? rotation, + required String title, + }) { + MarkerId markerId = MarkerId(id); + Marker marker = Marker( + markerId: markerId, + icon: descriptor, + infoWindow: InfoWindow( + title: title, + onTap: () { + int index = homeController.allNearestRestaurant.indexWhere( + (p0) => p0.id == id, + ); + Get.to( + const RestaurantDetailsScreen(), + arguments: { + "vendorModel": homeController.allNearestRestaurant[index], + }, + ); + }, + ), + position: LatLng(latitude ?? 0.0, longitude ?? 0.0), + rotation: rotation ?? 0.0, + ); + markers[markerId] = marker; + } +} diff --git a/lib/controllers/mobile_login_controller.dart b/lib/controllers/mobile_login_controller.dart new file mode 100644 index 0000000..d3fe4d6 --- /dev/null +++ b/lib/controllers/mobile_login_controller.dart @@ -0,0 +1,63 @@ +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../constant/constant.dart'; +import '../screen_ui/auth_screens/otp_verification_screen.dart'; + +class MobileLoginController extends GetxController { + final Rx mobileController = TextEditingController().obs; + final Rx countryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs; + + final FirebaseAuth _auth = FirebaseAuth.instance; + + /// Send OTP to the entered phone number + Future sendOtp() async { + final mobile = mobileController.value.text.trim(); + final countryCode = countryCodeController.value.text.trim(); + + if (mobile.isEmpty || mobile.length != 10) { + ShowToastDialog.showToast("Please enter a valid 10-digit mobile number".tr); + return; + } + + try { + ShowToastDialog.showLoader("Sending OTP...".tr); + + await _auth.verifyPhoneNumber( + phoneNumber: '$countryCode$mobile', + verificationCompleted: (PhoneAuthCredential credential) { + // Optionally handle auto-verification + }, + verificationFailed: (FirebaseAuthException e) { + ShowToastDialog.closeLoader(); + if (e.code == 'invalid-phone-number') { + ShowToastDialog.showToast("Invalid phone number".tr); + } else { + ShowToastDialog.showToast(e.message ?? "OTP verification failed".tr); + } + }, + codeSent: (String verificationId, int? resendToken) { + ShowToastDialog.closeLoader(); + Get.to(() => const OtpVerificationScreen(), arguments: {'countryCode': countryCode, 'phoneNumber': mobile, 'verificationId': verificationId}); + }, + codeAutoRetrievalTimeout: (String verificationId) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("OTP timed out. Please try again.".tr); + // Optional: Handle timeout + }, + ); + } catch (e) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Something went wrong. Please try again.".tr); + } + } + + @override + void onClose() { + mobileController.value.dispose(); + countryCodeController.value.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/my_booking_on_demand_controller.dart b/lib/controllers/my_booking_on_demand_controller.dart new file mode 100644 index 0000000..150f4d5 --- /dev/null +++ b/lib/controllers/my_booking_on_demand_controller.dart @@ -0,0 +1,73 @@ +import 'package:get/get.dart'; +import '../models/onprovider_order_model.dart'; +import '../models/worker_model.dart'; +import '../service/fire_store_utils.dart'; + +class MyBookingOnDemandController extends GetxController { + RxList orders = [].obs; + RxBool isLoading = true.obs; + + RxString selectedTab = "Placed".obs; + RxMap workers = {}.obs; + + RxList tabTitles = ["Placed", "Completed", "Cancelled"].obs; + + @override + void onInit() { + super.onInit(); + listenOrders(); // Listen for real-time updates + } + + + void selectTab(String tab) { + selectedTab.value = tab; + } + + void listenOrders() { + isLoading.value = true; + + FireStoreUtils.getProviderOrdersStream().listen( + (updatedOrders) { + orders.value = updatedOrders; + + // Fetch worker info if not already fetched + for (var order in updatedOrders) { + if (order.workerId != null && order.workerId!.isNotEmpty && !workers.containsKey(order.workerId!)) { + FireStoreUtils.getWorker(order.workerId!).then((worker) { + if (worker != null) workers[order.workerId!] = worker; + }); + } + } + + isLoading.value = false; + }, + onError: (error) { + print("Error fetching orders stream: $error"); + isLoading.value = false; + }, + ); + } + + List get filteredParcelOrders => getOrdersForTab(selectedTab.value); + + List getOrdersForTab(String tab) { + switch (tab) { + case "Placed": + return orders.where((order) => ["Order Placed", "Order Accepted", "Order Assigned", "Order Ongoing", "In Transit"].contains(order.status)).toList(); + + case "Completed": + return orders.where((order) => ["Order Completed"].contains(order.status)).toList(); + + case "Cancelled": + return orders.where((order) => ["Order Rejected", "Order Cancelled", "Driver Rejected"].contains(order.status)).toList(); + + default: + return []; + } + } + + WorkerModel? getWorker(String? workerId) { + if (workerId == null || workerId.isEmpty) return null; + return workers[workerId]; + } +} diff --git a/lib/controllers/my_cab_booking_controller.dart b/lib/controllers/my_cab_booking_controller.dart new file mode 100644 index 0000000..f2d6359 --- /dev/null +++ b/lib/controllers/my_cab_booking_controller.dart @@ -0,0 +1,904 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as maths; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cab_order_model.dart'; +import 'package:customer/models/payment_model/cod_setting_model.dart'; +import 'package:customer/models/payment_model/flutter_wave_model.dart'; +import 'package:customer/models/payment_model/mercado_pago_model.dart'; +import 'package:customer/models/payment_model/mid_trans.dart'; +import 'package:customer/models/payment_model/orange_money.dart'; +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:customer/models/payment_model/pay_stack_model.dart'; +import 'package:customer/models/payment_model/paypal_model.dart'; +import 'package:customer/models/payment_model/paytm_model.dart'; +import 'package:customer/models/payment_model/razorpay_model.dart'; +import 'package:customer/models/payment_model/stripe_model.dart'; +import 'package:customer/models/payment_model/wallet_setting_model.dart'; +import 'package:customer/models/payment_model/xendit.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/payment/MercadoPagoScreen.dart'; +import 'package:customer/payment/PayFastScreen.dart'; +import 'package:customer/payment/getPaytmTxtToken.dart'; +import 'package:customer/payment/midtrans_screen.dart'; +import 'package:customer/payment/orangePayScreen.dart'; +import 'package:customer/payment/paystack/pay_stack_screen.dart'; +import 'package:customer/payment/paystack/pay_stack_url_model.dart'; +import 'package:customer/payment/paystack/paystack_url_genrater.dart'; +import 'package:customer/payment/stripe_failed_model.dart'; +import 'package:customer/payment/xenditModel.dart'; +import 'package:customer/payment/xenditScreen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; + +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../themes/app_them_data.dart'; + +class MyCabBookingController extends GetxController { + RxBool isLoading = true.obs; + RxString selectedTab = "New".obs; + + RxList cabOrder = [].obs; + + final List tabKeys = ["New", "On Going", "Completed", "Cancelled"]; + + Rx userModel = UserModel().obs; + + @override + Future onInit() async { + super.onInit(); + fetchParcelData(); + } + + Future selectTab(String tab) async { + selectedTab.value = tab; + } + + Future fetchParcelData() async { + isLoading.value = true; + + if (FirebaseAuth.instance.currentUser != null) { + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((user) { + if (user != null) { + userModel.value = user; + } + }); + + FireStoreUtils.getCabDriverOrders().listen((orders) { + cabOrder.value = orders; + }); + + await getPaymentSettings(); + } + + isLoading.value = false; + } + + List get filteredParcelOrders => getOrdersForTab(selectedTab.value); + + List getOrdersForTab(String tab) { + switch (tab) { + case "New": + return cabOrder.where((order) => ["Order Placed", "Driver Pending"].contains(order.status)).toList(); + + case "On Going": + return cabOrder.where((order) => ["Driver Accepted", "Order Shipped", "In Transit"].contains(order.status)).toList(); + + case "Completed": + return cabOrder.where((order) => ["Order Completed"].contains(order.status)).toList(); + + case "Cancelled": + return cabOrder.where((order) => ["Order Rejected", "Order Cancelled", "Driver Rejected"].contains(order.status)).toList(); + + default: + return []; + } + } + + /// Get localized title for UI + String getLocalizedTabTitle(String tabKey) { + switch (tabKey) { + case "New": + return "New".tr; + case "On Going": + return "On Going".tr; + case "Completed": + return "Completed".tr; + case "Cancelled": + return "Cancelled".tr; + default: + return tabKey; + } + } + + String formatDate(Timestamp timestamp) { + final dateTime = timestamp.toDate(); + return DateFormat("dd MMM yyyy, hh:mm a").format(dateTime); + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + final RxString selectedPaymentMethod = ''.obs; + + Rx currentOrder = CabOrderModel().obs; + + Rx selectedOrder = CabOrderModel().obs; + RxDouble totalAmount = 0.0.obs; + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + + void calculateTotalAmount(CabOrderModel order) { + subTotal.value = 0.0; + discount.value = 0.0; + taxAmount.value = 0.0; + totalAmount.value = 0.0; + + selectedOrder.value = order; + try { + subTotal.value = double.tryParse(selectedOrder.value.subTotal?.toString() ?? "0") ?? 0.0; + discount.value = double.tryParse(selectedOrder.value.discount?.toString() ?? "0") ?? 0.0; + taxAmount.value = 0.0; + + subTotal.value = subTotal.value; + + if (selectedOrder.value.taxSetting != null) { + for (var element in selectedOrder.value.taxSetting!) { + taxAmount.value += Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element); + } + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + } catch (e) { + ShowToastDialog.showToast("Failed to calculate total: $e"); + } + } + + Future completeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.cod.name) { + selectedOrder.value.paymentMethod = selectedPaymentMethod.value; + await FireStoreUtils.cabOrderPlace(selectedOrder.value).then((value) { + ShowToastDialog.showToast("Payment method changed".tr); + Get.back(); + }); + } else { + selectedOrder.value.paymentMethod = selectedPaymentMethod.value; + userModel.value.inProgressOrderID ??= []; + userModel.value.inProgressOrderID!.clear(); + await FireStoreUtils.updateUser(userModel.value); + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: selectedOrder.value.id, + note: "Cab Amount debited".tr, + paymentStatus: "success".tr, + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()); + }); + } + selectedOrder.value.paymentStatus = true; + await FireStoreUtils.cabOrderPlace(selectedOrder.value).then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + Get.back(); + }); + } + } + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'eMart Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": Constant.userModel!.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": Constant.userModel!.email}, + "back_urls": { + "failure": "${Constant.globalUrl}payment/failure", + "pending": "${Constant.globalUrl}payment/pending", + "success": "${Constant.globalUrl}payment/success", + }, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + completeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen( + amount: (double.parse(totalAmount) * 100).toString(), + currency: "ZAR", + secretKey: payStackModel.value.secretKey.toString(), + userModel: Constant.userModel!, + ).then((value) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": Constant.userModel!.email.toString(), + "phonenumber": Constant.userModel!.phoneNumber, // Add a real phone number + "name": Constant.userModel!.fullName(), // Add a real customer name + }, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: Constant.userModel!).then(( + String? value, + ) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + // final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + // String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + // + // final response = await http.post( + // Uri.parse(getChecksum), + // headers: {}, + // body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + // ); + // + // final data = jsonDecode(response.body); + // await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + // initiatePayment(amount: amount, orderId: orderId).then((value) { + // String callback = ""; + // if (paytmModel.value.isSandboxEnabled == true) { + // callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } else { + // callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } + // + // GetPaymentTxtTokenModel result = value; + // startTransaction(context, txnTokenBy: result.body.txnToken, orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + // }); + // }); + } + + Future startTransaction( + context, { + required String txnTokenBy, + required orderId, + required double amount, + required callBackURL, + required isStaging, + }) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // completeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID.toString(), + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), + "checksum_value": checkSum, + }, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post( + Uri.parse(initiateURL), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }, + ); + log(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': Constant.userModel!.phoneNumber, 'email': Constant.userModel!.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse( + midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links', + ); + + final response = await http.post( + url, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!), + }, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to( + () => OrangeMoneyScreen( + initialURl: paymentURL, + accessToken: accessToken, + amount: amount, + orangePay: orangeMoneyModel.value, + orderId: orderId, + payToken: payToken, + ), + )!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: { + 'Authorization': "Basic ${orangeMoneyModel.value.auth!}", + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + body: requestBody, + ); + + // Handle the response + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + // ignore: use_build_context_synchronously + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({ + required String orderIdData, + required BuildContext context, + required String currency, + required String amountData, + }) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = + orangeMoneyModel.value.isSandbox! == true + ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' + : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to( + () => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()), + )!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/my_profile_controller.dart b/lib/controllers/my_profile_controller.dart new file mode 100644 index 0000000..566b012 --- /dev/null +++ b/lib/controllers/my_profile_controller.dart @@ -0,0 +1,51 @@ +import 'dart:developer'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import '../service/fire_store_utils.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; + +class MyProfileController extends GetxController { + RxBool isLoading = true.obs; + + RxString isDarkMode = "Light".obs; // For UI text + RxBool isDarkModeSwitch = false.obs; // For switch widget + + @override + void onInit() { + getTheme(); + super.onInit(); + } + + void getTheme() { + bool isDark = Preferences.getBoolean(Preferences.themKey); + isDarkMode.value = isDark ? "Dark" : "Light"; + isDarkModeSwitch.value = isDark; + isLoading.value = false; + } + + void toggleDarkMode(bool value) { + isDarkModeSwitch.value = value; + isDarkMode.value = value ? "Dark" : "Light"; + Preferences.setBoolean(Preferences.themKey, value); + + // Update ThemeController for instant app theme change + if (Get.isRegistered()) { + final themeController = Get.find(); + themeController.isDark.value = value; + } + } + + // Delete user API + Future deleteUserFromServer() async { + var url = '${Constant.websiteUrl}/api/delete-user'; + try { + var response = await http.post(Uri.parse(url), body: {'uuid': FireStoreUtils.getCurrentUid()}); + log("deleteUserFromServer :: ${response.body}"); + return response.statusCode == 200; + } catch (e) { + return false; + } + } +} diff --git a/lib/controllers/my_rental_booking_controller.dart b/lib/controllers/my_rental_booking_controller.dart new file mode 100644 index 0000000..1876d9b --- /dev/null +++ b/lib/controllers/my_rental_booking_controller.dart @@ -0,0 +1,228 @@ +import 'dart:async'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:get/get.dart'; +import '../models/rental_order_model.dart'; +import '../models/tax_model.dart'; +import '../service/fire_store_utils.dart'; + +class MyRentalBookingController extends GetxController { + RxBool isLoading = true.obs; + RxList rentalOrders = [].obs; + + RxString selectedTab = "New".obs; + RxList tabTitles = ["New", "On Going", "Completed", "Cancelled"].obs; + + StreamSubscription>? _rentalSubscription; + final RxString selectedPaymentMethod = ''.obs; + + @override + void onInit() { + super.onInit(); + listenRentalOrders(); + } + + void selectTab(String tab) { + selectedTab.value = tab; + } + + /// Start listening to rental orders live. Cancel previous subscription first. + void listenRentalOrders() { + isLoading.value = true; + _rentalSubscription?.cancel(); + if (Constant.userModel != null) { + _rentalSubscription = FireStoreUtils.getRentalOrders().listen( + (orders) { + rentalOrders.assignAll(orders); + }, + onError: (err) { + isLoading.value = false; + print("Error fetching rental orders: $err"); + }, + ); + } + isLoading.value = false; + + } + + Rx selectedOrder = RentalOrderModel().obs; + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + RxDouble extraKilometerCharge = 0.0.obs; + RxDouble extraMinutesCharge = 0.0.obs; + + void calculateTotalAmount(RentalOrderModel order) { + subTotal.value = 0.0; + discount.value = 0.0; + taxAmount.value = 0.0; + totalAmount.value = 0.0; + extraKilometerCharge.value = 0.0; + extraMinutesCharge.value = 0.0; + + selectedOrder.value = order; + try { + subTotal.value = double.tryParse(selectedOrder.value.subTotal?.toString() ?? "0") ?? 0.0; + discount.value = double.tryParse(selectedOrder.value.discount?.toString() ?? "0") ?? 0.0; + taxAmount.value = 0.0; + + if (selectedOrder.value.endTime != null) { + DateTime start = selectedOrder.value.startTime!.toDate(); + DateTime end = selectedOrder.value.endTime!.toDate(); + int hours = end.difference(start).inHours; + if (hours >= int.parse(selectedOrder.value.rentalPackageModel!.includedHours.toString())) { + hours = hours - int.parse(selectedOrder.value.rentalPackageModel!.includedHours.toString()); + double hourlyRate = double.tryParse(selectedOrder.value.rentalPackageModel?.extraMinuteFare?.toString() ?? "0") ?? 0.0; + extraMinutesCharge.value = (hours * 60) * hourlyRate; + } + } + + if (selectedOrder.value.startKitoMetersReading != null && selectedOrder.value.endKitoMetersReading != null) { + double startKm = double.tryParse(selectedOrder.value.startKitoMetersReading?.toString() ?? "0") ?? 0.0; + double endKm = double.tryParse(selectedOrder.value.endKitoMetersReading?.toString() ?? "0") ?? 0.0; + if (endKm > startKm) { + double totalKm = endKm - startKm; + if (totalKm > double.parse(selectedOrder.value.rentalPackageModel!.includedDistance!)) { + totalKm = totalKm - double.parse(selectedOrder.value.rentalPackageModel!.includedDistance!); + double extraKmRate = double.tryParse(selectedOrder.value.rentalPackageModel?.extraKmFare?.toString() ?? "0") ?? 0.0; + extraKilometerCharge.value = totalKm * extraKmRate; + } + } + } + subTotal.value = subTotal.value + extraKilometerCharge.value + extraMinutesCharge.value; + + if (selectedOrder.value.taxSetting != null) { + for (var element in selectedOrder.value.taxSetting!) { + taxAmount.value += Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element); + } + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + } catch (e) { + ShowToastDialog.showToast("Failed to calculate total: $e"); + } + } + + Future completeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.cod.name) { + selectedOrder.value.paymentMethod = selectedPaymentMethod.value; + await FireStoreUtils.rentalOrderPlace(selectedOrder.value).then((value) { + ShowToastDialog.showToast("Payment method changed".tr); + Get.back(); + Get.back(); + }); + } else { + selectedOrder.value.paymentStatus = true; + selectedOrder.value.paymentMethod = selectedPaymentMethod.value; + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: selectedOrder.value.id, + note: "Rental Amount debited".tr, + paymentStatus: "success".tr, + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()); + } + }); + } + + await FireStoreUtils.rentalOrderPlace(selectedOrder.value).then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + Get.back(); + Get.back(); + }); + } + } + + /// Return filtered list for a specific tab title + List getOrdersForTab(String tab) { + switch (tab) { + case "New": + return rentalOrders.where((order) => ["Order Placed", "Order Accepted", "Driver Pending"].contains(order.status)).toList(); + + case "On Going": + return rentalOrders.where((order) => ["Driver Accepted", "Order Shipped", "In Transit"].contains(order.status)).toList(); + + case "Completed": + return rentalOrders.where((order) => ["Order Completed"].contains(order.status)).toList(); + + case "Cancelled": + return rentalOrders.where((order) => ["Order Rejected", "Order Cancelled", "Driver Rejected"].contains(order.status)).toList(); + + default: + return []; + } + } + + /// Old helper (optional) + List get filteredRentalOrders => getOrdersForTab(selectedTab.value); + + Future cancelRentalRequest(RentalOrderModel order, {List? taxList}) async { + try { + isLoading.value = true; + order.status = Constant.orderCancelled; + await FireStoreUtils.rentalOrderPlace(order); + + listenRentalOrders(); + + if (order.paymentMethod?.toLowerCase() != "cod") { + double totalTax = 0.0; + + if (taxList != null) { + for (var element in taxList) { + totalTax += Constant.calculateTax( + amount: (double.parse(order.subTotal.toString()) - double.parse(order.discount.toString())).toString(), + taxModel: element, + ); + } + } + + double subTotal = double.parse(order.subTotal.toString()) - double.parse(order.discount.toString()); + double refundAmount = subTotal + totalTax; + + WalletTransactionModel walletTransaction = WalletTransactionModel( + id: Constant.getUuid(), + amount: refundAmount, + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: true, + // refund + orderId: order.id, + note: "Refund for cancelled booking".tr, + paymentStatus: "success".tr, + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(walletTransaction); + await FireStoreUtils.updateUserWallet(amount: refundAmount.toString(), userId: FireStoreUtils.getCurrentUid()); + } + ShowToastDialog.showToast("Booking cancelled successfully".tr); + } catch (e) { + ShowToastDialog.showToast("Failed to cancel booking: $e".tr); + } finally { + isLoading.value = false; + } + } + + @override + void onClose() { + _rentalSubscription?.cancel(); + super.onClose(); + } +} diff --git a/lib/controllers/on_boarding_controller.dart b/lib/controllers/on_boarding_controller.dart new file mode 100644 index 0000000..178fbaf --- /dev/null +++ b/lib/controllers/on_boarding_controller.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../models/on_boarding_model.dart'; +import '../service/fire_store_utils.dart'; + +class OnboardingController extends GetxController { + RxInt currentPage = 0.obs; + late PageController pageController; + + RxBool isLoading = true.obs; + RxList onboardingList = [].obs; + + @override + void onInit() { + super.onInit(); + pageController = PageController(); + getOnBoardingData(); + } + + void nextPage() { + if (currentPage.value < onboardingList.length - 1) { + pageController.nextPage(duration: 300.milliseconds, curve: Curves.ease); + } + } + + void onPageChanged(int index) { + currentPage.value = index; + } + + Future getOnBoardingData() async { + isLoading.value = true; + await FireStoreUtils.getOnBoardingList().then((value) { + onboardingList.value = value; + }); + isLoading.value = false; + } + + @override + void onClose() { + pageController.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/on_demand_booking_controller.dart b/lib/controllers/on_demand_booking_controller.dart new file mode 100644 index 0000000..ab33fbd --- /dev/null +++ b/lib/controllers/on_demand_booking_controller.dart @@ -0,0 +1,214 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../models/onprovider_order_model.dart'; +import '../models/provider_serivce_model.dart'; +import '../screen_ui/on_demand_service/on_demand_dashboard_screen.dart'; +import '../screen_ui/on_demand_service/on_demand_payment_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../service/send_notification.dart'; +import '../themes/show_toast_dialog.dart'; +import 'on_demand_dashboard_controller.dart'; + +class OnDemandBookingController extends GetxController { + Rxn provider = Rxn(); + RxString categoryTitle = ''.obs; + + RxInt quantity = 1.obs; + Rx descriptionController = TextEditingController().obs; + Rx dateTimeController = TextEditingController().obs; + Rx couponTextController = TextEditingController().obs; + + Rx selectedDateTime = DateTime.now().obs; + RxString dateTimeText = "".obs; + + RxList couponList = [].obs; + + RxDouble subTotal = 0.0.obs; + RxDouble price = 0.0.obs; + RxDouble discountAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + RxString discountType = "".obs; + RxString discountLabel = "".obs; + RxString offerCode = "".obs; + + Rx selectedAddress = ShippingAddress().obs; + + @override + void onInit() { + super.onInit(); + final Map? args = Get.arguments; + if (args != null) { + provider.value = args['providerModel']; + categoryTitle.value = args['categoryTitle'] ?? ''; + } + selectedAddress.value = Constant.selectedLocation; + fetchCoupons(); + calculatePrice(); + } + + void fetchCoupons() { + if (provider.value?.author != null && provider.value!.author!.isNotEmpty) { + FireStoreUtils.getProviderCoupon(provider.value!.author!).then((activeCoupons) => couponList.assignAll(activeCoupons)); + FireStoreUtils.getProviderCouponAfterExpire(provider.value!.author!).then((expiredCoupons) => couponList.addAll(expiredCoupons)); + } + } + + void incrementQuantity() { + quantity.value++; + calculatePrice(); + } + + void decrementQuantity() { + if (quantity.value > 1) { + quantity.value--; + calculatePrice(); + } + } + + void setDateTime(DateTime dateTime) { + selectedDateTime.value = dateTime; + dateTimeText.value = DateFormat('dd-MM-yyyy HH:mm').format(dateTime); + dateTimeController.value.text = dateTimeText.value; + } + + void applyCoupon(CouponModel coupon) { + double discount = 0.0; + if (coupon.discountType == "Percentage" || coupon.discountType == "Percent") { + discount = price.value * (double.tryParse(coupon.discount.toString()) ?? 0) / 100; + } else { + discount = double.tryParse(coupon.discount.toString()) ?? 0; + } + + if (subTotal.value > discount) { + discountType.value = coupon.discountType ?? ''; + discountLabel.value = coupon.discount.toString(); + offerCode.value = coupon.code ?? ''; + calculatePrice(); + } else { + Get.snackbar("Error", "Coupon cannot be applied"); + } + } + + String getDate(String date) { + try { + DateTime dt = DateTime.parse(date); + return "${dt.day}-${dt.month}-${dt.year}"; + } catch (e) { + return date; + } + } + + void calculatePrice() { + double basePrice = + (provider.value?.disPrice == "" || provider.value?.disPrice == "0") + ? double.tryParse(provider.value?.price.toString() ?? "0") ?? 0 + : double.tryParse(provider.value?.disPrice.toString() ?? "0") ?? 0; + + price.value = basePrice * quantity.value; + + // discount + if (discountType.value == "Percentage" || discountType.value == "Percent") { + discountAmount.value = price.value * (double.tryParse(discountLabel.value) ?? 0) / 100; + } else { + discountAmount.value = double.tryParse(discountLabel.value.isEmpty ? '0' : discountLabel.value) ?? 0; + } + + subTotal.value = price.value - discountAmount.value; + + // tax calculation + double total = subTotal.value; + for (var element in Constant.taxList) { + total += Constant.getTaxValue(amount: subTotal.value.toString(), taxModel: element); + } + + totalAmount.value = total; + } + + Future confirmBooking(BuildContext context) async { + if (selectedAddress.value.getFullAddress().isEmpty) { + ShowToastDialog.showToast("Please enter address".tr); + } else if (dateTimeController.value.text.isEmpty) { + ShowToastDialog.showToast("Please select time slot.".tr); + } else { + UserModel? providerUser = await FireStoreUtils.getUserProfile(provider.value!.author!); + + if (provider.value?.priceUnit == "Fixed") { + OnProviderOrderModel onDemandOrderModel = OnProviderOrderModel( + authorID: FireStoreUtils.getCurrentUid(), + author: Constant.userModel!, + quantity: double.parse(quantity.value.toString()), + sectionId: Constant.sectionConstantModel!.id, + address: selectedAddress.value, + taxModel: Constant.taxList, + provider: provider.value, + status: Constant.orderPlaced, + scheduleDateTime: Timestamp.fromDate(selectedDateTime.value), + notes: descriptionController.value.text, + discount: discountAmount.toString(), + discountType: discountType.toString(), + discountLabel: discountLabel.toString(), + adminCommission: + Constant.sectionConstantModel?.adminCommision?.isEnabled == false + ? '0' + : "${providerUser?.adminCommissionModel?.amount ?? Constant.sectionConstantModel?.adminCommision?.amount ?? 0}", + adminCommissionType: + Constant.sectionConstantModel?.adminCommision?.isEnabled == false + ? 'fixed' + : providerUser?.adminCommissionModel?.commissionType ?? Constant.sectionConstantModel?.adminCommision?.commissionType, + otp: Constant.getReferralCode(), + couponCode: offerCode.toString(), + ); + print('totalAmount ::::::: ${double.tryParse(Constant.amountShow(amount: totalAmount.value.toString())) ?? 0.0}'); + print('totalAmount value ::::::: ${totalAmount.value}'); + + Get.to(() => OnDemandPaymentScreen(), arguments: {'onDemandOrderModel': Rxn(onDemandOrderModel), 'totalAmount': totalAmount.value, 'isExtra': false}); + } else { + ShowToastDialog.showLoader("Please wait...".tr); + OnProviderOrderModel onDemandOrder = OnProviderOrderModel( + otp: Constant.getReferralCode(), + authorID: FireStoreUtils.getCurrentUid(), + author: Constant.userModel!, + sectionId: Constant.sectionConstantModel!.id, + address: selectedAddress.value, + taxModel: Constant.taxList, + status: Constant.orderPlaced, + createdAt: Timestamp.now(), + quantity: double.parse(quantity.value.toString()), + provider: provider.value, + extraPaymentStatus: true, + scheduleDateTime: Timestamp.fromDate(selectedDateTime.value), + notes: descriptionController.value.text, + adminCommission: + Constant.sectionConstantModel?.adminCommision?.isEnabled == false + ? '0' + : "${providerUser?.adminCommissionModel?.amount ?? Constant.sectionConstantModel?.adminCommision?.amount ?? 0}", + adminCommissionType: + Constant.sectionConstantModel?.adminCommision?.isEnabled == false + ? 'fixed' + : providerUser?.adminCommissionModel?.commissionType ?? Constant.sectionConstantModel?.adminCommision?.commissionType, + paymentStatus: true, + ); + + await FireStoreUtils.onDemandOrderPlace(onDemandOrder, 0.0); + await FireStoreUtils.sendOrderOnDemandServiceEmail(orderModel: onDemandOrder); + + if (providerUser != null) { + Map payLoad = {"type": 'provider_order', "orderId": onDemandOrder.id}; + await SendNotification.sendFcmMessage(Constant.bookingPlaced, providerUser.fcmToken.toString(), payLoad); + } + + ShowToastDialog.closeLoader(); + Get.offAll(const OnDemandDashboardScreen()); + OnDemandDashboardController controller = Get.put(OnDemandDashboardController()); + controller.selectedIndex.value = 2; + ShowToastDialog.showToast("OnDemand Service successfully booked".tr); + } + } + } +} diff --git a/lib/controllers/on_demand_category_controller.dart b/lib/controllers/on_demand_category_controller.dart new file mode 100644 index 0000000..31e6da0 --- /dev/null +++ b/lib/controllers/on_demand_category_controller.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; + +import '../models/category_model.dart'; +import '../service/fire_store_utils.dart'; + +class OnDemandCategoryController extends GetxController { + RxBool isLoading = true.obs; + RxList categories = [].obs; + + @override + void onInit() { + super.onInit(); + fetchCategories(); + } + + void fetchCategories() async { + try { + isLoading.value = true; + // Fetch categories + FireStoreUtils.getOnDemandCategory().then((catValue) { + categories.value = catValue; + }); + } catch (e) { + print("Error fetching categories: $e"); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/controllers/on_demand_dashboard_controller.dart b/lib/controllers/on_demand_dashboard_controller.dart new file mode 100644 index 0000000..33b395c --- /dev/null +++ b/lib/controllers/on_demand_dashboard_controller.dart @@ -0,0 +1,40 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/screen_ui/on_demand_service/favourite_ondemand_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +import '../screen_ui/on_demand_service/my_booking_on_demand_screen.dart'; +import '../screen_ui/on_demand_service/on_demand_home_screen.dart'; + +class OnDemandDashboardController extends GetxController { + RxInt selectedIndex = 0.obs; + + RxList pageList = [].obs; + + @override + void onInit() { + getTaxList(); + if (Constant.walletSetting == false) { + pageList.value = [OnDemandHomeScreen(), FavouriteOndemandScreen(), const MyBookingOnDemandScreen(), const ProfileScreen()]; + } else { + pageList.value = [ + OnDemandHomeScreen(), + FavouriteOndemandScreen(), + const MyBookingOnDemandScreen(), + const WalletScreen(), + const ProfileScreen(), + ]; + } + super.onInit(); + } + + Future getTaxList() async { + await FireStoreUtils.getTaxList(Constant.sectionConstantModel!.id).then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + } +} diff --git a/lib/controllers/on_demand_details_controller.dart b/lib/controllers/on_demand_details_controller.dart new file mode 100644 index 0000000..8f7ea01 --- /dev/null +++ b/lib/controllers/on_demand_details_controller.dart @@ -0,0 +1,100 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../models/favorite_ondemand_service_model.dart'; +import '../models/provider_serivce_model.dart'; +import '../models/rating_model.dart'; +import '../service/fire_store_utils.dart'; + +class OnDemandDetailsController extends GetxController { + late ProviderServiceModel provider; + + final Rxn userModel = Rxn(); + final RxString subCategoryTitle = ''.obs; + final RxString categoryTitle = ''.obs; + final RxList ratingService = [].obs; + final RxList lstFav = [].obs; + final RxBool isLoading = true.obs; + final RxBool isOpen = false.obs; + final RxString tabString = "About".obs; + + @override + void onInit() { + super.onInit(); + provider = Get.arguments['providerModel']; + timeCheck(); + getData(); + } + + + Future getData() async { + await getReviewList(); + await getAuthor(); //fetch and set provider author here + if (Constant.userModel != null) { + lstFav.value = await FireStoreUtils.getFavouritesServiceList(FireStoreUtils.getCurrentUid()); + } + isLoading.value = false; + } + + Future getReviewList() async { + await FireStoreUtils.getCategoryById(provider.categoryId.toString()).then((value) { + if (value != null) { + categoryTitle.value = value.title.toString(); + } + }); + + await FireStoreUtils.getSubCategoryById(provider.subCategoryId.toString()).then((value) { + if (value != null) { + subCategoryTitle.value = value.title.toString(); + } + }); + + await FireStoreUtils.getReviewByProviderServiceId(provider.id.toString()).then((value) { + ratingService.value = value; + }); + + if (Constant.userModel != null) { + await FireStoreUtils.getFavouritesServiceList(FireStoreUtils.getCurrentUid()).then((value) { + lstFav.value = value; + }); + } + } + + Future getAuthor() async { + final authorId = provider.author?.toString(); + if (authorId != null && authorId.isNotEmpty) { + final user = await FireStoreUtils.getUserProfile(authorId); + if (user != null) { + userModel.value = user; + } + } + } + + void timeCheck() { + final now = DateTime.now(); + final day = DateFormat('EEEE', 'en_US').format(now); + final date = DateFormat('dd-MM-yyyy').format(now); + + for (var element in provider.days) { + if (day == element.toString()) { + final start = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${provider.startTime}"); + final end = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${provider.endTime}"); + if (isCurrentDateInRange(start, end)) { + isOpen.value = true; + } + } + } + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + void changeTab(String tab) { + tabString.value = tab; + } +} + + diff --git a/lib/controllers/on_demand_home_controller.dart b/lib/controllers/on_demand_home_controller.dart new file mode 100644 index 0000000..16f4aac --- /dev/null +++ b/lib/controllers/on_demand_home_controller.dart @@ -0,0 +1,142 @@ +import 'package:customer/models/banner_model.dart'; +import 'package:customer/models/category_model.dart'; +import 'package:customer/models/favorite_ondemand_service_model.dart'; +import 'package:customer/models/provider_serivce_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import '../constant/constant.dart'; + +class OnDemandHomeController extends GetxController { + RxBool isLoading = true.obs; + RxList bannerTopHome = [].obs; + RxList categories = [].obs; + RxList providerList = [].obs; + + /// Store last fetched category + Rx categoryModel = Rx(null); + + @override + void onInit() { + getData(); + super.onInit(); + } + + Future getData() async { + isLoading.value = true; + await getZone(); + + // Fetch banners + FireStoreUtils.getHomeTopBanner().then((value) { + bannerTopHome.value = value; + }); + + // Fetch categories + FireStoreUtils.getOnDemandCategory().then((catValue) { + categories.value = catValue; + }); + + // Fetch provider services + FireStoreUtils.getProviderFuture() + .then((providerServiceList) { + Set uniqueAuthorIds = providerServiceList.map((service) => service.author).toSet(); + List listOfUniqueProviders = uniqueAuthorIds.toList(); + + List filteredProviders = []; + + for (var provider in listOfUniqueProviders) { + List filteredList = providerServiceList.where((service) => service.author == provider).toList(); + + filteredList.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); + + for (int index = 0; index < filteredList.length; index++) { + final service = filteredList[index]; + + if (Constant.isSubscriptionModelApplied == true || Constant.sectionConstantModel?.adminCommision?.isEnabled == true) { + if (service.subscriptionPlan?.itemLimit == "-1") { + filteredProviders.add(service); + } else { + if (index < int.parse(service.subscriptionPlan?.itemLimit ?? '0')) { + filteredProviders.add(service); + } + } + } else { + filteredProviders.add(service); + } + } + } + + providerList.value = filteredProviders; + isLoading.value = false; + }) + .catchError((e) { + print("Provider error: $e"); + isLoading.value = false; + }); + + FireStoreUtils.getFavouritesServiceList(FireStoreUtils.getCurrentUid()).then((favList) { + lstFav.value = favList; + }); + } + + /// Get category by id safely from cached categories + Future getCategory(String? categoryId) async { + if (categoryId == null || categoryId.isEmpty) return null; + + // Try to find category from cached list + CategoryModel? cat = categories.firstWhereOrNull((element) => element.id == categoryId); + + // If not found, fetch from Firestore + cat ??= await FireStoreUtils.getCategoryById(categoryId); + + categoryModel.value = cat; + return cat; + } + + RxList lstFav = [].obs; + + void toggleFavourite(ProviderServiceModel provider) { + if (Constant.userModel == null) { + Get.to(LoginScreen()); + } else { + var contain = lstFav.where((element) => element.service_id == provider.id); + if (contain.isNotEmpty) { + FavouriteOndemandServiceModel favouriteModel = FavouriteOndemandServiceModel( + section_id: provider.sectionId, + service_id: provider.id, + user_id: FireStoreUtils.getCurrentUid(), + serviceAuthorId: provider.author, + ); + FireStoreUtils.removeFavouriteOndemandService(favouriteModel); + lstFav.removeWhere((item) => item.service_id == provider.id); + } else { + FavouriteOndemandServiceModel favouriteModel = FavouriteOndemandServiceModel( + section_id: provider.sectionId, + service_id: provider.id, + user_id: FireStoreUtils.getCurrentUid(), + serviceAuthorId: provider.author, + ); + FireStoreUtils.setFavouriteOndemandSection(favouriteModel); + lstFav.add(favouriteModel); + } + } + } + + Future getZone() async { + await FireStoreUtils.getZone().then((value) { + if (value != null) { + for (int i = 0; i < value.length; i++) { + if (Constant.isPointInPolygon(LatLng(Constant.selectedLocation.location?.latitude ?? 0.0, Constant.selectedLocation.location?.longitude ?? 0.0), value[i].area!)) { + Constant.selectedZone = value[i]; + Constant.isZoneAvailable = true; + break; + } else { + Constant.selectedZone = value[i]; + Constant.isZoneAvailable = false; + } + } + } + }); + } +} diff --git a/lib/controllers/on_demand_order_details_controller.dart b/lib/controllers/on_demand_order_details_controller.dart new file mode 100644 index 0000000..2fb2d76 --- /dev/null +++ b/lib/controllers/on_demand_order_details_controller.dart @@ -0,0 +1,254 @@ +import 'dart:developer'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import '../constant/constant.dart'; +import '../models/onprovider_order_model.dart'; +import '../models/wallet_transaction_model.dart'; +import '../models/worker_model.dart'; +import '../service/fire_store_utils.dart'; +import '../service/send_notification.dart'; +import '../themes/show_toast_dialog.dart'; + +class OnDemandOrderDetailsController extends GetxController { + Rx providerUser = Rx(null); + Rxn onProviderOrder = Rxn(); + Rxn worker = Rxn(); + + Rx couponTextController = TextEditingController().obs; + Rx cancelBookingController = TextEditingController().obs; + + RxDouble subTotal = 0.0.obs; + RxDouble price = 0.0.obs; + RxDouble discountAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + RxDouble quantity = 0.0.obs; + + RxString discountType = "".obs; + RxString discountLabel = "".obs; + RxString offerCode = "".obs; + + RxList couponList = [].obs; + + final RxBool isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + + final args = Get.arguments; + if (args != null && args is OnProviderOrderModel) { + onProviderOrder.value = args; + } + getData(); + } + + Future getData() async { + try { + final order = await FireStoreUtils.getProviderOrderById(onProviderOrder.value!.id); + if (order != null) { + onProviderOrder.value = order; + + discountType.value = order.discountType ?? ""; + discountLabel.value = order.discountLabel ?? ""; + discountAmount.value = double.tryParse(order.discount.toString()) ?? 0.0; + offerCode.value = order.couponCode ?? ""; + + // Fetch provider + providerUser.value = await FireStoreUtils.getUserProfile(order.provider.author.toString()); + + // Fetch worker (if exists) + if (order.workerId != null && order.workerId!.isNotEmpty) { + worker.value = await FireStoreUtils.getWorker(order.workerId!); + } else { + worker.value = null; + } + + calculatePrice(); + + // Load available coupons + FireStoreUtils.getProviderCouponAfterExpire(order.provider.author.toString()).then((expiredCoupons) { + couponList.assignAll(expiredCoupons); + }); + } else { + onProviderOrder.value = null; + providerUser.value = null; + worker.value = null; + couponList.clear(); + } + } catch (e, st) { + log("Error in getData: $e\n$st"); + onProviderOrder.value = null; + providerUser.value = null; + worker.value = null; + couponList.clear(); + } + } + + void applyCoupon(CouponModel coupon) { + double discount = 0.0; + if (coupon.discountType == "Percentage" || coupon.discountType == "Percent") { + discount = price.value * (double.tryParse(coupon.discount.toString()) ?? 0) / 100; + } else { + discount = double.tryParse(coupon.discount.toString()) ?? 0; + } + + if (subTotal.value > discount) { + discountType.value = coupon.discountType ?? ''; + discountLabel.value = coupon.discount.toString(); + offerCode.value = coupon.code ?? ''; + calculatePrice(); + } else { + Get.snackbar("Error", "Coupon cannot be applied"); + } + } + + void calculatePrice() { + double basePrice = + (onProviderOrder.value?.provider.disPrice == "" || onProviderOrder.value?.provider.disPrice == "0") + ? double.tryParse(onProviderOrder.value?.provider.price.toString() ?? "0") ?? 0 + : double.tryParse(onProviderOrder.value?.provider.disPrice.toString() ?? "0") ?? 0; + + price.value = basePrice * (onProviderOrder.value?.quantity ?? 0.0); + + // discount + if (discountType.value == "Percentage" || discountType.value == "Percent") { + discountAmount.value = price.value * (double.tryParse(discountLabel.value) ?? 0) / 100; + } else { + discountAmount.value = double.tryParse(discountLabel.value.isEmpty ? '0' : discountLabel.value) ?? 0; + } + + subTotal.value = price.value - discountAmount.value; + + // tax calculation + double total = subTotal.value; + for (var element in Constant.taxList) { + total += Constant.getTaxValue(amount: subTotal.value.toString(), taxModel: element); + } + + totalAmount.value = total; + } + + String getDate(String date) { + try { + DateTime dt = DateTime.parse(date); + return "${dt.day}-${dt.month}-${dt.year}"; + } catch (e) { + return date; + } + } + + Future cancelBooking() async { + final order = onProviderOrder.value; + if (order == null) return; + + ShowToastDialog.showLoader("Please wait...".tr); + + try { + double total = 0.0; + + // Calculate total + final pricePerUnit = + (order.provider.disPrice == "" || order.provider.disPrice == "0") ? double.tryParse(order.provider.price.toString()) ?? 0 : double.tryParse(order.provider.disPrice.toString()) ?? 0; + + total = pricePerUnit * (order.quantity); + + // Add tax + if (Constant.taxList.isNotEmpty) { + for (var tax in Constant.taxList) { + total += Constant.getTaxValue(amount: total.toString(), taxModel: tax); + } + } + + // Admin commission + double adminComm = 0.0; + if ((order.adminCommission ?? '0') != '0' && (order.adminCommissionType ?? '').isNotEmpty) { + if (order.adminCommissionType!.toLowerCase() == 'percentage' || order.adminCommissionType!.toLowerCase() == 'percent') { + adminComm = (total * (double.tryParse(order.adminCommission!) ?? 0)) / 100; + } else { + adminComm = double.tryParse(order.adminCommission!) ?? 0; + } + } + + // Refund customer wallet if not COD + if ((order.payment_method).toLowerCase() != 'cod') { + await FireStoreUtils.setWalletTransaction( + WalletTransactionModel( + id: Constant.getUuid(), + serviceType: 'ondemand-service', + amount: total, + date: Timestamp.now(), + paymentMethod: 'wallet', + transactionUser: 'customer', + userId: Constant.userModel?.id, + isTopup: true, + orderId: order.id, + note: 'Booking Amount Refund', + paymentStatus: "success".tr, + ), + ); + + // Deduct from provider if accepted + if (order.status == Constant.orderAccepted) { + await FireStoreUtils.setWalletTransaction( + WalletTransactionModel( + id: Constant.getUuid(), + serviceType: 'ondemand-service', + amount: total, + date: Timestamp.now(), + paymentMethod: 'wallet', + transactionUser: 'provider', + userId: order.provider.author ?? '', + isTopup: false, + orderId: order.id, + note: 'Booking Amount Refund', + paymentStatus: "success".tr, + ), + ); + } + } + + // Refund admin commission + if (order.status == Constant.orderAccepted && adminComm > 0) { + await FireStoreUtils.setWalletTransaction( + WalletTransactionModel( + id: Constant.getUuid(), + serviceType: 'ondemand-service', + amount: adminComm, + date: Timestamp.now(), + paymentMethod: 'wallet', + transactionUser: 'provider', + userId: order.provider.author ?? '', + isTopup: true, + orderId: order.id, + note: 'Admin commission refund', + paymentStatus: "success".tr, + ), + ); + } + + // Update order status & reason + order.status = Constant.orderCancelled; + order.reason = cancelBookingController.value.text; + + await FireStoreUtils.updateOnDemandOrder(order); // Ensure this completes + + // Notify provider + final provider = await FireStoreUtils.getUserProfile(order.provider.author ?? ''); + if (provider != null) { + Map payload = {"type": 'provider_order', "orderId": order.id}; + await SendNotification.sendFcmMessage(Constant.bookingPlaced, provider.fcmToken ?? '', payload); + } + + ShowToastDialog.closeLoader(); + Get.back(); + ShowToastDialog.showToast("Booking cancelled successfully".tr); + } catch (e, st) { + log("Cancel error: $e\n$st"); + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Something went wrong".tr); + } + } +} diff --git a/lib/controllers/on_demand_review_controller.dart b/lib/controllers/on_demand_review_controller.dart new file mode 100644 index 0000000..4ccccd2 --- /dev/null +++ b/lib/controllers/on_demand_review_controller.dart @@ -0,0 +1,169 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../constant/collection_name.dart'; +import '../models/onprovider_order_model.dart'; +import '../models/provider_serivce_model.dart'; +import '../models/rating_model.dart'; +import '../models/worker_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; +import '../constant/constant.dart'; +import '../themes/show_toast_dialog.dart'; + +class OnDemandReviewController extends GetxController { + final Rxn order = Rxn(); + final RxString reviewFor = "".obs; + final Rxn ratingModel = Rxn(); + final RxDouble ratings = 0.0.obs; + final TextEditingController comment = TextEditingController(); + + final Rxn provider = Rxn(); + final Rxn providerServiceModel = Rxn(); + final Rxn workerModel = Rxn(); + + final RxInt providerReviewCount = 0.obs; + final RxDouble providerReviewSum = 0.0.obs; + final RxInt serviceReviewCount = 0.obs; + final RxDouble serviceReviewSum = 0.0.obs; + final RxInt workerReviewCount = 0.obs; + final RxDouble workerReviewSum = 0.0.obs; + + final FirebaseFirestore firestore = FirebaseFirestore.instance; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + if (args != null) { + order.value = args['order']; + reviewFor.value = args['reviewFor']; + getReview(); + } + } + + void getReview() async { + // Get existing rating + if (reviewFor.value == "Provider") { + RatingModel? value = await FireStoreUtils.getReviewsByProviderID(order.value!.id, order.value!.provider.author.toString()); + if (value != null) { + ratingModel.value = value; + ratings.value = value.rating ?? 0.0; + comment.text = value.comment ?? ''; + } + } else { + RatingModel? value = await FireStoreUtils.getReviewsByWorkerID(order.value!.id, order.value!.workerId.toString()); + if (value != null) { + ratingModel.value = value; + ratings.value = value.rating ?? 0.0; + comment.text = value.comment ?? ''; + } + } + + // Worker review logic + if (reviewFor.value == "Worker") { + WorkerModel? value = await FireStoreUtils.getWorker(order.value!.workerId.toString()); + if (value != null) { + workerModel.value = value; + + final int existingCount = (value.reviewsCount ?? 0).toInt(); + final double existingSum = (value.reviewsSum ?? 0.0).toDouble(); + final double oldRating = ratingModel.value?.rating ?? 0.0; + + workerReviewCount.value = ratingModel.value != null ? (existingCount - 1) : existingCount; + workerReviewSum.value = ratingModel.value != null ? (existingSum - oldRating) : existingSum; + } + } + // Provider & service review logic + else { + UserModel? user = await FireStoreUtils.getUserProfile(order.value!.provider.author.toString()); + if (user != null) { + provider.value = user; + + final int existingCount = int.tryParse(user.reviewsCount?.toString() ?? '0') ?? 0; + final double existingSum = double.tryParse(user.reviewsSum?.toString() ?? '0.0') ?? 0.0; + final double oldRating = ratingModel.value?.rating ?? 0.0; + + providerReviewCount.value = ratingModel.value != null ? (existingCount - 1) : existingCount; + providerReviewSum.value = ratingModel.value != null ? (existingSum - oldRating) : existingSum; + } + + ProviderServiceModel? service = await FireStoreUtils.getCurrentProvider(order.value!.provider.id.toString()); + if (service != null) { + providerServiceModel.value = service; + + final int existingCount = (service.reviewsCount ?? 0).toInt(); + final double existingSum = (service.reviewsSum ?? 0.0).toDouble(); + final double oldRating = ratingModel.value?.rating ?? 0.0; + + serviceReviewCount.value = ratingModel.value != null ? (existingCount - 1) : existingCount; + serviceReviewSum.value = ratingModel.value != null ? (existingSum - oldRating) : existingSum; + } + } + } + + void submitReview() async { + if (reviewFor.value == "Provider") { + await _providerReviewSubmit(); + } else { + await _workerReviewSubmit(); + } + } + + Future _providerReviewSubmit() async { + ShowToastDialog.showLoader("Submit in...".tr); + providerServiceModel.value!.reviewsCount = serviceReviewCount.value + 1; + providerServiceModel.value!.reviewsSum = serviceReviewSum.value + ratings.value; + + // Convert to string only if your model field is String + provider.value!.reviewsCount = (providerReviewCount.value + 1).toString(); + provider.value!.reviewsSum = (providerReviewSum.value + ratings.value).toString(); + + RatingModel rate = RatingModel( + id: ratingModel.value?.id ?? firestore.collection(CollectionName.itemsReview).doc().id, + productId: ratingModel.value?.productId ?? order.value!.provider.id, + comment: comment.text, + photos: ratingModel.value?.photos ?? [], + rating: ratings.value, + orderId: ratingModel.value?.orderId ?? order.value!.id, + vendorId: ratingModel.value?.vendorId ?? order.value!.provider.author.toString(), + customerId: Constant.userModel?.id, + uname: '${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}', + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(rate); + await FireStoreUtils.updateUser(provider.value!); + await FireStoreUtils.updateProvider(providerServiceModel.value!); + + ShowToastDialog.closeLoader(); + Get.back(result: true); + } + + Future _workerReviewSubmit() async { + ShowToastDialog.showLoader("Submit in...".tr); + workerModel.value!.reviewsCount = workerReviewCount.value + 1; + workerModel.value!.reviewsSum = workerReviewSum.value + ratings.value; + + RatingModel rate = RatingModel( + id: ratingModel.value?.id ?? firestore.collection(CollectionName.itemsReview).doc().id, + productId: ratingModel.value?.productId ?? order.value!.provider.id, + comment: comment.text, + photos: ratingModel.value?.photos ?? [], + rating: ratings.value, + orderId: ratingModel.value?.orderId ?? order.value!.id, + driverId: ratingModel.value?.driverId ?? order.value!.workerId.toString(), + customerId: Constant.userModel?.id, + uname: '${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}', + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(rate); + await FireStoreUtils.updateWorker(workerModel.value!); + + ShowToastDialog.closeLoader(); + Get.back(result: true); + } +} diff --git a/lib/controllers/order_controller.dart b/lib/controllers/order_controller.dart new file mode 100644 index 0000000..c6b606b --- /dev/null +++ b/lib/controllers/order_controller.dart @@ -0,0 +1,51 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/order_model.dart'; +import '../service/cart_provider.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class OrderController extends GetxController { + RxList allList = [].obs; + RxList inProgressList = [].obs; + RxList deliveredList = [].obs; + RxList rejectedList = [].obs; + RxList cancelledList = [].obs; + + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getOrder(); + super.onInit(); + } + + Future getOrder() async { + if (Constant.userModel != null) { + await FireStoreUtils.getAllOrder().then((value) { + allList.value = value; + + rejectedList.value = allList.where((p0) => p0.status == Constant.orderRejected).toList(); + inProgressList.value = + allList + .where( + (p0) => p0.status == Constant.orderAccepted || p0.status == Constant.driverPending || p0.status == Constant.orderShipped || p0.status == Constant.orderInTransit, + ) + .toList(); + + deliveredList.value = allList.where((p0) => p0.status == Constant.orderCompleted).toList(); + cancelledList.value = allList.where((p0) => p0.status == Constant.orderCancelled).toList(); + }); + } + + isLoading.value = false; + } + + final CartProvider cartProvider = CartProvider(); + + void addToCart({required CartProductModel cartProductModel}) { + cartProvider.addToCart(Get.context!, cartProductModel, cartProductModel.quantity!); + update(); + } +} diff --git a/lib/controllers/order_details_controller.dart b/lib/controllers/order_details_controller.dart new file mode 100644 index 0000000..44c2898 --- /dev/null +++ b/lib/controllers/order_details_controller.dart @@ -0,0 +1,77 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/order_model.dart'; +import 'package:get/get.dart'; + +import '../service/cart_provider.dart'; + +class OrderDetailsController extends GetxController { + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Rx orderModel = OrderModel().obs; + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + orderModel.value = argumentData['orderModel']; + } + calculatePrice(); + update(); + } + + RxDouble subTotal = 0.0.obs; + RxDouble specialDiscountAmount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + Future calculatePrice() async { + subTotal.value = 0.0; + specialDiscountAmount.value = 0.0; + taxAmount.value = 0.0; + totalAmount.value = 0.0; + + for (var element in orderModel.value.products!) { + if (double.parse(element.discountPrice.toString()) <= 0) { + subTotal.value = subTotal.value + + double.parse(element.price.toString()) * double.parse(element.quantity.toString()) + + (double.parse(element.extrasPrice.toString()) * double.parse(element.quantity.toString())); + } else { + subTotal.value = subTotal.value + + double.parse(element.discountPrice.toString()) * double.parse(element.quantity.toString()) + + (double.parse(element.extrasPrice.toString()) * double.parse(element.quantity.toString())); + } + } + + if (orderModel.value.specialDiscount != null && orderModel.value.specialDiscount!['special_discount'] != null) { + specialDiscountAmount.value = double.parse(orderModel.value.specialDiscount!['special_discount'].toString()); + } + + if (orderModel.value.taxSetting != null) { + for (var element in orderModel.value.taxSetting!) { + taxAmount.value = taxAmount.value + + Constant.calculateTax(amount: (subTotal.value - double.parse(orderModel.value.discount.toString()) - specialDiscountAmount.value).toString(), taxModel: element); + } + } + + totalAmount.value = (subTotal.value - double.parse(orderModel.value.discount.toString()) - specialDiscountAmount.value) + + taxAmount.value + + double.parse(orderModel.value.deliveryCharge.toString()) + + double.parse(orderModel.value.tipAmount.toString()); + + isLoading.value = false; + } + + final CartProvider cartProvider = CartProvider(); + + void addToCart({required CartProductModel cartProductModel}) { + cartProvider.addToCart(Get.context!, cartProductModel, cartProductModel.quantity!); + update(); + } +} diff --git a/lib/controllers/order_placing_controller.dart b/lib/controllers/order_placing_controller.dart new file mode 100644 index 0000000..fa489d6 --- /dev/null +++ b/lib/controllers/order_placing_controller.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'package:customer/models/order_model.dart'; +import 'package:get/get.dart'; +import '../service/database_helper.dart'; + +class OrderPlacingController extends GetxController { + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + startTimer(); + super.onInit(); + } + + Rx orderModel = OrderModel().obs; + + Future getArgument() async { + DatabaseHelper.instance.deleteAllCartProducts(); + dynamic argumentData = Get.arguments; + if (argumentData != null) { + orderModel.value = argumentData['orderModel']; + } + isLoading.value = false; + update(); + } + + Timer? timer; + RxInt counter = 0.obs; + + RxBool isPlacing = false.obs; + + void startTimer() { + timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (counter.value == 3) { + timer.cancel(); + isPlacing.value = true; + } + counter++; + }); + } +} diff --git a/lib/controllers/osm_search_place_controller.dart b/lib/controllers/osm_search_place_controller.dart new file mode 100644 index 0000000..7cdf1e2 --- /dev/null +++ b/lib/controllers/osm_search_place_controller.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter_osm_plugin/flutter_osm_plugin.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class OsmSearchPlaceController extends GetxController { + Rx searchTxtController = TextEditingController().obs; + RxList suggestionsList = [].obs; + + @override + void onInit() { + super.onInit(); + searchTxtController.value.addListener(() { + _onChanged(); + }); + } + + void _onChanged() { + fetchAddress(searchTxtController.value.text); + } + + Future fetchAddress(text) async { + log(":: fetchAddress :: $text"); + try { + String locale = 'en'; + SharedPreferences sp = await SharedPreferences.getInstance(); + if (sp.getString("languageCode") != null || sp.getString("languageCode")?.isNotEmpty == true) { + locale = sp.getString("languageCode") ?? "en"; + } + suggestionsList.value = await addressSuggestion(text, locale: locale); + } catch (e) { + log(e.toString()); + } + } +} diff --git a/lib/controllers/otp_verification_controller.dart b/lib/controllers/otp_verification_controller.dart new file mode 100644 index 0000000..d9309f5 --- /dev/null +++ b/lib/controllers/otp_verification_controller.dart @@ -0,0 +1,131 @@ +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../constant/constant.dart'; +import '../models/user_model.dart'; +import '../screen_ui/auth_screens/login_screen.dart'; +import '../screen_ui/auth_screens/sign_up_screen.dart'; +import '../screen_ui/service_home_screen/service_list_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../utils/notification_service.dart'; + +class OtpVerifyController extends GetxController { + /// Use a normal controller (NOT obs) + final Rx otpController = TextEditingController().obs; + + /// Reactive Strings + final RxString countryCode = "".obs; + final RxString phoneNumber = "".obs; + final RxString verificationId = "".obs; + RxInt resendToken = 0.obs; + + final FirebaseAuth _auth = FirebaseAuth.instance; + + @override + void onInit() { + super.onInit(); + + final args = Get.arguments ?? {}; + + countryCode.value = args['countryCode'] ?? ""; + phoneNumber.value = args['phoneNumber'] ?? ""; + verificationId.value = args['verificationId'] ?? ""; + } + + Future sendOTP() async { + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: countryCode.value + phoneNumber.value, + verificationCompleted: (PhoneAuthCredential credential) {}, + verificationFailed: (FirebaseAuthException e) {}, + codeSent: (String verificationId0, int? resendToken0) async { + verificationId.value = verificationId0; + resendToken.value = resendToken0!; + ShowToastDialog.showToast("OTP sent".tr); + }, + timeout: const Duration(seconds: 25), + forceResendingToken: resendToken.value, + codeAutoRetrievalTimeout: (String verificationId0) { + verificationId0 = verificationId.value; + }, + ); + return true; + } + + void verifyOtp() async { + if (otpController.value.text.length != 6) { + ShowToastDialog.showToast("Enter valid 6-digit OTP".tr); + return; + } + + try { + ShowToastDialog.showLoader("Verifying OTP...".tr); + + final credential = PhoneAuthProvider.credential(verificationId: verificationId.value, smsCode: otpController.value.text.trim()); + + final fcmToken = await NotificationService.getToken(); + final result = await _auth.signInWithCredential(credential); + + if (result.additionalUserInfo?.isNewUser == true) { + final userModel = UserModel(id: result.user!.uid, countryCode: countryCode.value, phoneNumber: phoneNumber.value, fcmToken: fcmToken, active: true); + ShowToastDialog.closeLoader(); + Get.to(() => const SignUpScreen(), arguments: {'type': 'mobileNumber', 'userModel': userModel}); + return; + } + + final exists = await FireStoreUtils.userExistOrNot(result.user!.uid); + ShowToastDialog.closeLoader(); + + if (!exists) { + final userModel = UserModel(id: result.user!.uid, countryCode: countryCode.value, phoneNumber: phoneNumber.value, fcmToken: fcmToken); + Get.off(() => const SignUpScreen(), arguments: {'type': 'mobileNumber', 'userModel': userModel}); + return; + } + + final userModel = await FireStoreUtils.getUserProfile(result.user!.uid); + if (userModel == null || userModel.role != 'customer') { + await _auth.signOut(); + Get.offAll(() => const LoginScreen()); + return; + } + + if (userModel.active == false) { + ShowToastDialog.showToast("This user is disabled".tr); + await _auth.signOut(); + Get.offAll(() => const LoginScreen()); + return; + } + + userModel.fcmToken = fcmToken; + await FireStoreUtils.updateUser(userModel); + + if (userModel.shippingAddress?.isNotEmpty ?? false) { + final defaultAddress = userModel.shippingAddress!.firstWhere((e) => e.isDefault == true, orElse: () => userModel.shippingAddress!.first); + Constant.selectedLocation = defaultAddress; + + Get.offAll(() => const ServiceListScreen()); + } else { + Get.offAll(() => const LocationPermissionScreen()); + } + } catch (e) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Invalid OTP or Verification Failed".tr); + } + } + + String maskPhoneNumber(String phone) { + if (phone.length < 4) return phone; + + final first = phone.substring(0, 2); + final last = phone.substring(phone.length - 2); + return "$first*** ***$last"; + } + + @override + void dispose() { + otpController.value.dispose(); + // TODO: implement dispose + super.dispose(); + } +} diff --git a/lib/controllers/parcel_coupon_controller.dart b/lib/controllers/parcel_coupon_controller.dart new file mode 100644 index 0000000..89da091 --- /dev/null +++ b/lib/controllers/parcel_coupon_controller.dart @@ -0,0 +1,29 @@ +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class ParcelCouponController extends GetxController{ + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + + void getData(){ + getCouponCode(); + } + RxBool isLoading = true.obs; + RxList cabCouponList = [].obs; + + Future getCouponCode() async { + await FireStoreUtils.getParcelCoupon().then((value) { + cabCouponList.value = value; + // Handle the retrieved coupon code + }); + print("cabCouponList ${cabCouponList.length}"); + isLoading.value = false; + } +} \ No newline at end of file diff --git a/lib/controllers/parcel_dashboard_controller.dart b/lib/controllers/parcel_dashboard_controller.dart new file mode 100644 index 0000000..bc9b6a5 --- /dev/null +++ b/lib/controllers/parcel_dashboard_controller.dart @@ -0,0 +1,35 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/screen_ui/parcel_service/home_parcel_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; +import '../screen_ui/parcel_service/my_booking_screen.dart'; + +class ParcelDashboardController extends GetxController { + RxInt selectedIndex = 0.obs; + + RxList pageList = [].obs; + + @override + void onInit() { + getTaxList(); + if (Constant.walletSetting == false) { + pageList.value = [const HomeParcelScreen(), const MyBookingScreen(), const ProfileScreen()]; + } else { + pageList.value = [const HomeParcelScreen(), const MyBookingScreen(), const WalletScreen(), const ProfileScreen()]; + } + super.onInit(); + } + + Future getTaxList() async { + await FireStoreUtils.getTaxList(Constant.sectionConstantModel!.id).then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + } + + DateTime? currentBackPressTime; + RxBool canPopNow = false.obs; +} diff --git a/lib/controllers/parcel_my_booking_controller.dart b/lib/controllers/parcel_my_booking_controller.dart new file mode 100644 index 0000000..3e2addd --- /dev/null +++ b/lib/controllers/parcel_my_booking_controller.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../constant/constant.dart'; +import '../models/parcel_order_model.dart'; +import '../models/wallet_transaction_model.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; + +class ParcelMyBookingController extends GetxController { + RxBool isLoading = true.obs; + RxList parcelOrder = [].obs; + + RxString selectedTab = "New".obs; + RxList tabTitles = ["New", "In Transit", "Delivered", "Cancelled"].obs; + + StreamSubscription>? _parcelSubscription; + + @override + void onInit() { + super.onInit(); + listenParcelOrders(); + } + + void selectTab(String tab) { + selectedTab.value = tab; + } + + /// Start listening to orders live. Cancel previous subscription first. + void listenParcelOrders() { + isLoading.value = true; + if (Constant.userModel == null) { + isLoading.value = false; + return; + } + _parcelSubscription?.cancel(); + _parcelSubscription = FireStoreUtils.listenParcelOrders().listen( + (orders) { + parcelOrder.assignAll(orders); + isLoading.value = false; + }, + onError: (err) { + isLoading.value = false; + // optionally handle error + }, + ); + } + + /// Return filtered list for a specific tab title + List getOrdersForTab(String tab) { + switch (tab) { + case "New": + return parcelOrder.where((order) => ["Order Placed"].contains(order.status)).toList(); + + case "In Transit": + return parcelOrder.where((order) => ["Order Accepted", "Driver Accepted", "Driver Pending", "Order Shipped", "In Transit"].contains(order.status)).toList(); + + case "Delivered": + return parcelOrder.where((order) => ["Order Completed"].contains(order.status)).toList(); + + case "Cancelled": + return parcelOrder.where((order) => ["Order Rejected", "Order Cancelled", "Driver Rejected"].contains(order.status)).toList(); + + default: + return []; + } + } + + /// Old helper (optional) + List get filteredParcelOrders => getOrdersForTab(selectedTab.value); + + String formatDate(Timestamp timestamp) { + final dateTime = timestamp.toDate(); + return DateFormat("dd MMM yyyy, hh:mm a").format(dateTime); + } + + Future cancelParcelOrder(ParcelOrderModel order) async { + try { + isLoading.value = true; + + if (order.status != Constant.orderPlaced) { + ShowToastDialog.showToast("You can only cancel before pickup.".tr); + return; + } + + order.status = Constant.orderCancelled; + await FireStoreUtils.parcelOrderPlace(order); + + listenParcelOrders(); + + if (order.paymentMethod?.toLowerCase() != "cod") { + double totalTax = 0.0; + + final taxSettings = order.taxSetting ?? []; + + for (var element in taxSettings) { + totalTax += Constant.calculateTax(amount: (double.parse(order.subTotal.toString()) - double.parse(order.discount.toString())).toString(), taxModel: element); + } + + double subTotal = double.parse(order.subTotal.toString()) - double.parse(order.discount.toString()); + double refundAmount = subTotal + totalTax; + + WalletTransactionModel walletTransaction = WalletTransactionModel( + id: Constant.getUuid(), + amount: refundAmount, + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: true, + // refund + orderId: order.id, + note: "Refund for cancelled parcel order", + paymentStatus: "success", + serviceType: Constant.parcelServiceType, + ); + + // Save wallet transaction + await FireStoreUtils.setWalletTransaction(walletTransaction); + + // Update wallet balance + await FireStoreUtils.updateUserWallet(amount: refundAmount.toString(), userId: FireStoreUtils.getCurrentUid()); + } + + ShowToastDialog.showToast("Order cancelled successfully".tr); + } catch (e) { + ShowToastDialog.showToast("${'Failed to cancel order:'.tr} $e".tr); + } finally { + isLoading.value = false; + } + } + + @override + void onClose() { + _parcelSubscription?.cancel(); + super.onClose(); + } +} diff --git a/lib/controllers/parcel_order_confirmation_controller.dart b/lib/controllers/parcel_order_confirmation_controller.dart new file mode 100644 index 0000000..7c61e0b --- /dev/null +++ b/lib/controllers/parcel_order_confirmation_controller.dart @@ -0,0 +1,983 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as maths; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; +import '../../../models/parcel_order_model.dart'; +import '../constant/constant.dart'; +import '../models/payment_model/cod_setting_model.dart'; +import '../models/payment_model/flutter_wave_model.dart'; +import '../models/payment_model/mercado_pago_model.dart'; +import '../models/payment_model/mid_trans.dart'; +import '../models/payment_model/orange_money.dart'; +import '../models/payment_model/pay_fast_model.dart'; +import '../models/payment_model/pay_stack_model.dart'; +import '../models/payment_model/paypal_model.dart'; +import '../models/payment_model/paytm_model.dart'; +import '../models/payment_model/razorpay_model.dart'; +import '../models/payment_model/stripe_model.dart'; +import '../models/payment_model/wallet_setting_model.dart'; +import '../models/payment_model/xendit.dart'; +import '../models/user_model.dart'; +import '../payment/MercadoPagoScreen.dart'; +import '../payment/PayFastScreen.dart'; +import '../payment/getPaytmTxtToken.dart'; +import '../payment/midtrans_screen.dart'; +import '../payment/orangePayScreen.dart'; +import '../payment/paystack/pay_stack_screen.dart'; +import '../payment/paystack/pay_stack_url_model.dart'; +import '../payment/paystack/paystack_url_genrater.dart'; +import '../payment/stripe_failed_model.dart'; +import '../payment/xenditModel.dart'; +import '../payment/xenditScreen.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../screen_ui/parcel_service/order_successfully_placed.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/app_them_data.dart'; +import '../themes/show_toast_dialog.dart'; +import '../utils/preferences.dart'; + +class ParcelOrderConfirmationController extends GetxController { + RxBool isLoading = true.obs; + final Rx parcelOrder = ParcelOrderModel().obs; + final RxList images = [].obs; + final RxString paymentBy = "Receiver".obs; + + RxString selectedPaymentMethod = ''.obs; + RxBool isOrderPlaced = false.obs; + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + Rx couponController = TextEditingController().obs; + Rx userModel = UserModel().obs; + + @override + void onInit() { + super.onInit(); + getArgument(); + } + + Rx selectedCouponModel = CouponModel().obs; + + Future getArgument() async { + final dynamic args = Get.arguments; + if (args != null) { + parcelOrder.value = args['parcelOrder']; + images.value = List.from(args['images'] ?? []); + calculatePrice(); + } + + userModel.value = Constant.userModel!; + await fetchCoupons(); + await getPaymentSettings(); + isLoading.value = false; + update(); + } + + void calculatePrice() { + subTotal.value = 0; + discount.value = 0; + taxAmount.value = 0; + + subTotal.value = double.tryParse(parcelOrder.value.subTotal ?? '0') ?? 0.0; + + if (selectedCouponModel.value.id != null) { + discount.value = Constant.calculateDiscount(amount: subTotal.value.toString(), offerModel: selectedCouponModel.value); + } + + for (var element in Constant.taxList) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + + print("Tax: ${taxAmount.value}"); + print("Discount: ${discount.value}"); + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + } + + RxList couponList = [].obs; + + Future fetchCoupons() async { + try { + await FireStoreUtils.getParcelCoupon().then((value) { + couponList.value = value; + }); + } catch (e) { + print("Error fetching coupons: $e"); + } + } + + String formatDate(Timestamp timestamp) { + final dateTime = timestamp.toDate(); + return DateFormat("dd MMM yyyy, hh:mm a").format(dateTime); + } + + Future placeOrder() async { + ShowToastDialog.showLoader("Please wait...".tr); + + try { + List parcelImages = []; + if (images.isNotEmpty) { + for (var image in images) { + final upload = await FireStoreUtils.uploadChatImageToFireStorage(File(image.path), Get.context!); + parcelImages.add(upload.url); + } + } + + parcelOrder.value.parcelImages = parcelImages; + parcelOrder.value.discount = discount.value.toString(); + parcelOrder.value.discountType = selectedCouponModel.value.discountType.toString(); + parcelOrder.value.discountLabel = selectedCouponModel.value.code.toString(); + parcelOrder.value.adminCommission = Constant.sectionConstantModel?.adminCommision?.amount?.toString(); + parcelOrder.value.adminCommissionType = Constant.sectionConstantModel?.adminCommision?.commissionType; + parcelOrder.value.status = Constant.orderPlaced; + parcelOrder.value.createdAt = Timestamp.now(); + parcelOrder.value.author = userModel.value; + parcelOrder.value.authorID = FireStoreUtils.getCurrentUid(); + parcelOrder.value.paymentMethod = paymentBy.value == "Receiver" ? "cod" : selectedPaymentMethod.value; + parcelOrder.value.paymentCollectByReceiver = paymentBy.value == "Receiver"; + parcelOrder.value.senderZoneId = Constant.getZoneId(parcelOrder.value.senderLatLong!.latitude ?? 0.0, parcelOrder.value.senderLatLong!.longitude ?? 0.0); + parcelOrder.value.receiverZoneId = Constant.getZoneId(parcelOrder.value.receiverLatLong!.latitude ?? 0.0, parcelOrder.value.receiverLatLong!.longitude ?? 0.0); + + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.value.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: parcelOrder.value.id, + note: "Parcel Amount debited", + paymentStatus: "success", + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.value.toString()}", userId: FireStoreUtils.getCurrentUid()); + } + }); + } + await FireStoreUtils.parcelOrderPlace(parcelOrder.value).then((value) async { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Order placed successfully".tr); + Get.offAll(() => OrderSuccessfullyPlaced(), arguments: {'parcelOrder': parcelOrder.value}); + await FireStoreUtils.sendParcelBookEmail(orderModel: parcelOrder.value); + }); + } catch (e) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Something went wrong. Please try again.".tr); + } + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'eMart Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": Constant.userModel?.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": Constant.userModel?.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, BuildContext context) { + // ✅ Ensure amount format is correct (e.g. 10.00) + final formattedAmount = double.parse(amount).toStringAsFixed(2); + + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.emart.customer://paypalpay", + cancelURL: "com.emart.customer://paypalcancel", + + transactions: [ + { + "amount": { + "total": formattedAmount, + "currency": "USD", + "details": {"subtotal": formattedAmount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + debugPrint("✅ PayPal Payment Success: $params"); + placeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + debugPrint("❌ PayPal Payment Error: $error"); + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + debugPrint("⚠️ PayPal Payment Canceled: $params"); + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + // void paypalPaymentSheet(String amount, context) { + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: + // (BuildContext context) => UsePaypal( + // sandboxMode: payPalModel.value.isLive == true ? false : true, + // clientId: payPalModel.value.paypalClient ?? '', + // secretKey: payPalModel.value.paypalSecret ?? '', + // returnURL: "https://success.emart.com/return", + // cancelURL: "https://cancel.emart.com/cancel", + // // returnURL: "com.emart.customer://paypalpay", + // // cancelURL: "com.emart.customer://paypalpay", + // transactions: [ + // { + // "amount": { + // "total": amount, + // "currency": "USD", + // "details": {"subtotal": amount}, + // }, + // }, + // ], + // note: "Contact us for any questions on your order.", + // onSuccess: (Map params) async { + // placeOrder(); + // ShowToastDialog.showToast("Payment Successful!!".tr); + // }, + // onError: (error) { + // Get.back(); + // ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + // }, + // onCancel: (params) { + // Get.back(); + // ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + // }, + // ), + // ), + // ); + // } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + // Convert to int (kobo/cents) + int amountInCents = (double.parse(totalAmount) * 100).round(); + + await PayStackURLGen.payStackURLGen( + amount: amountInCents.toString(), //integer string + currency: "ZAR", + secretKey: payStackModel.value.secretKey.toString(), + userModel: Constant.userModel!, + ).then((value) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + ///flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + setRef(); // make sure you generate reference + + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": {"email": Constant.userModel?.email.toString(), "phonenumber": Constant.userModel?.phoneNumber, "name": Constant.userModel?.fullName()}, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) async { + bool isVerified = await verifyFlutterWavePayment(_ref!); + + if (isVerified) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + Get.back(); + } + }); + } else { + debugPrint('Payment initialization failed: ${response.body}'); + } + } + + Future verifyFlutterWavePayment(String txRef) async { + try { + final url = Uri.parse("https://api.flutterwave.com/v3/transactions/verify_by_reference?tx_ref=$txRef"); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final response = await http.get(url, headers: headers); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data['status'] == 'success' && data['data']['status'] == 'successful') { + return true; // ✅ Payment confirmed + } + } + return false; // ❌ Payment not verified + } catch (e) { + debugPrint("Error verifying payment: $e"); + return false; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: Constant.userModel!).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + placeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + + Future getPaytmCheckSum(context, {required double amount}) async { + final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + ); + + final data = jsonDecode(response.body); + await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + initiatePayment(amount: amount, orderId: orderId).then((value) { + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + + GetPaymentTxtTokenModel result = value; + startTransaction(context, txnTokenBy: result.body.txnToken ?? '', orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + }); + }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // placeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), "checksum_value": checkSum}, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required String orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + + String callback = + (paytmModel.value.isSandboxEnabled ?? false) ? "https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId" : "https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + + print("INITIATE PAYMENT CALL:"); + print("MID: ${paytmModel.value.paytmMID}"); + print("OrderId: $orderId"); + print("Amount: $amount"); + print("Env: ${(paytmModel.value.isSandboxEnabled ?? false) ? "STAGING" : "LIVE"}"); + + final response = await http.post( + Uri.parse(initiateURL), + body: { + "mid": paytmModel.value.paytmMID ?? "", + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY ?? "", + "amount": amount.toStringAsFixed(0), // Paytm requires integer + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": (paytmModel.value.isSandboxEnabled ?? false) ? "1" : "0", + }, + ); + + log("Paytm Initiate Response: ${response.body}"); + + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + + return GetPaymentTxtTokenModel.fromJson(data); + } + + // Future initiatePayment({required double amount, required orderId}) async { + // String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + // String callback = ""; + // if (paytmModel.value.isSandboxEnabled == true) { + // callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } else { + // callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } + // final response = await http.post( + // Uri.parse(initiateURL), + // headers: {}, + // body: { + // "mid": paytmModel.value.paytmMID, + // "order_id": orderId, + // "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + // "amount": amount.toString(), + // "currency": "INR", + // "callback_url": callback, + // "custId": FireStoreUtils.getCurrentUid(), + // "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + // }, + // ); + // log(response.body); + // final data = jsonDecode(response.body); + // if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + // Get.back(); + // ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + // } + // return GetPaymentTxtTokenModel.fromJson(data); + // } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': Constant.userModel?.phoneNumber, 'email': Constant.userModel?.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + // 🟠 ORANGE MONEY PAYMENT INTEGRATION + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + debugPrint('🟩 Starting OrangePay Payment...'); + debugPrint('💰 Amount: $amount | 🆔 Order ID: $id'); + + ShowToastDialog.showLoader("Initializing payment...".tr); + + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + + ShowToastDialog.closeLoader(); + + if (paymentURL.toString().isNotEmpty) { + debugPrint('✅ Payment URL fetched successfully: $paymentURL'); + + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))?.then((value) async { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + debugPrint('🎉 Payment Successful for Order ID: $orderId'); + + if (Get.isBottomSheetOpen ?? false) Get.back(); + await placeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + debugPrint('⚠️ Payment flow closed without success.'); + + if (Get.isBottomSheetOpen ?? false) Get.back(); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + if (Get.isBottomSheetOpen ?? false) Get.back(); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + const String apiUrl = 'https://api.orange.com/oauth/v3/token'; + final Map requestBody = {'grant_type': 'client_credentials'}; + + debugPrint('🔐 Fetching access token from Orange API...'); + debugPrint('📡 POST $apiUrl'); + + final response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + debugPrint('🔍 Response Code: ${response.statusCode}'); + debugPrint('📨 Response Body: ${response.body}'); + + if (response.statusCode == 200) { + final Map responseData = jsonDecode(response.body); + accessToken = responseData['access_token']; + debugPrint('✅ Access Token Received: $accessToken'); + + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + debugPrint('❌ Failed to fetch access token.'); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + + String apiUrl = orangeMoneyModel.value.isSandbox == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + + // ✅ Ensure amount formatted correctly + String formattedAmount = double.parse(amountData).toStringAsFixed(2); + + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": formattedAmount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifyUrl ?? '', + }; + + debugPrint('💳 Creating Web Payment...'); + debugPrint('📡 POST $apiUrl'); + debugPrint('📦 Request Body: ${jsonEncode(requestBody)}'); + + final response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + debugPrint('🔍 Response Code: ${response.statusCode}'); + debugPrint('📨 Response Body: ${response.body}'); + + if (response.statusCode == 201) { + final Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + debugPrint('✅ Payment Token: $payToken'); + debugPrint('🌍 Payment URL: ${responseData['payment_url']}'); + return responseData['payment_url']; + } else { + debugPrint('⚠️ Unexpected message: ${responseData['message']}'); + return ''; + } + } else { + debugPrint('❌ Payment request failed.'); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + debugPrint('🌀 OrangePay reset completed.'); + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + placeOrder(); + (); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/parcel_order_details_controller.dart b/lib/controllers/parcel_order_details_controller.dart new file mode 100644 index 0000000..e82d9b8 --- /dev/null +++ b/lib/controllers/parcel_order_details_controller.dart @@ -0,0 +1,139 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/rating_model.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../constant/constant.dart'; +import '../models/parcel_category.dart'; +import '../models/parcel_order_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; + +class ParcelOrderDetailsController extends GetxController { + Rx parcelOrder = ParcelOrderModel().obs; + RxList parcelCategory = [].obs; + RxBool isLoading = false.obs; + + Rx driverUser = Rx(null); + Rx ratingModel = RatingModel().obs; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + if (args != null && args is ParcelOrderModel) { + parcelOrder.value = args; + setStatusHistoryFromString(parcelOrder.value); + } + loadParcelCategories(); + calculateTotalAmount(); + fetchDriverDetails(); + } + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + + void calculateTotalAmount() { + taxAmount = 0.0.obs; + discount = 0.0.obs; + subTotal.value = double.parse(parcelOrder.value.subTotal.toString()); + discount.value = double.parse(parcelOrder.value.discount ?? '0.0'); + + for (var element in parcelOrder.value.taxSetting!) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + update(); + } + + Future fetchDriverDetails() async { + if (parcelOrder.value.driverId != null) { + await FireStoreUtils.getUserProfile(parcelOrder.value.driverId ?? '').then((value) { + if (value != null) { + driverUser.value = value; + } + }); + + await FireStoreUtils.getReviewsbyID(parcelOrder.value.id.toString()).then((value) { + if (value != null) { + ratingModel.value = value; + } + }); + } + } + + void setStatusHistoryFromString(ParcelOrderModel order) { + final steps = ["Order Placed", "Driver Accepted", "Pickup Done", "In Transit", "Delivered"]; + + final history = []; + + DateTime baseTime = order.createdAt?.toDate() ?? DateTime.now(); + int minutesGap = 30; + + for (int i = 0; i < steps.length; i++) { + final step = steps[i]; + + history.add(ParcelStatus(status: step, time: baseTime.add(Duration(minutes: i * minutesGap)))); + + if (step == order.status) break; + } + + order.statusHistory = history; + } + + Future cancelParcelOrder() async { + ShowToastDialog.showLoader("Cancelling order...".tr); + parcelOrder.value.status = Constant.orderCancelled; + if (parcelOrder.value.paymentMethod?.toLowerCase() != "cod") { + WalletTransactionModel walletTransaction = WalletTransactionModel( + id: Constant.getUuid(), + amount: totalAmount.value, + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: true, + orderId: parcelOrder.value.id, + note: "Refund for cancelled parcel order", + paymentStatus: "success", + serviceType: Constant.parcelServiceType, + ); + + // Save wallet transaction + await FireStoreUtils.setWalletTransaction(walletTransaction); + + // Update wallet balance + await FireStoreUtils.updateUserWallet(amount: totalAmount.value.toString(), userId: FireStoreUtils.getCurrentUid()); + } + + await FireStoreUtils.parcelOrderPlace(parcelOrder.value); + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Order cancelled successfully".tr); + Get.back(result: true); + } + + void loadParcelCategories() async { + isLoading.value = true; + final categories = await FireStoreUtils.getParcelServiceCategory(); + parcelCategory.value = categories; + isLoading.value = false; + } + + String formatDate(Timestamp timestamp) { + final dateTime = timestamp.toDate(); + return DateFormat("dd MMM yyyy, hh:mm a").format(dateTime); + } + + ParcelCategory? getSelectedCategory() { + try { + return parcelCategory.firstWhere((cat) => cat.title?.toLowerCase().trim() == parcelOrder.value.parcelType?.toLowerCase().trim(), orElse: () => ParcelCategory()); + } catch (e) { + return null; + } + } +} diff --git a/lib/controllers/parcel_review_controller.dart b/lib/controllers/parcel_review_controller.dart new file mode 100644 index 0000000..becae1f --- /dev/null +++ b/lib/controllers/parcel_review_controller.dart @@ -0,0 +1,135 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/parcel_order_model.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../models/rating_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; +import '../constant/constant.dart'; +import '../themes/show_toast_dialog.dart'; + +class ParcelReviewController extends GetxController { + RxBool isLoading = true.obs; + + /// Order from arguments + final Rx order = Rx(null); + + /// Rating data + final Rx ratingModel = Rx(null); + final RxDouble ratings = 0.0.obs; + final Rx comment = TextEditingController().obs; + + /// Driver (to be reviewed) + final Rx driverUser = Rx(null); + + /// Review stats + final RxInt futureCount = 0.obs; + final RxInt futureSum = 0.obs; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + if (args != null && args['order'] != null) { + order.value = args['order'] as ParcelOrderModel; + getReview(); + } + } + + /// Fetch old review + driver stats + Future getReview() async { + await FireStoreUtils.getReviewsbyID(order.value?.id ?? "").then((value) { + if (value != null) { + ratingModel.value = value; + ratings.value = value.rating ?? 0; + comment.value.text = value.comment ?? ""; + } + }); + + await FireStoreUtils.getUserProfile(order.value?.driverId ?? '').then((value) { + if (value != null) { + driverUser.value = value; + + final int userReviewsCount = int.tryParse(driverUser.value!.reviewsCount?.toString() ?? "0") ?? 0; + final int userReviewsSum = int.tryParse(driverUser.value!.reviewsSum?.toString() ?? "0") ?? 0; + + if (ratingModel.value != null) { + final int oldRating = ratingModel.value?.rating?.toInt() ?? 0; + futureCount.value = userReviewsCount - 1; + futureSum.value = userReviewsSum - oldRating; + } else { + futureCount.value = userReviewsCount; + futureSum.value = userReviewsSum; + } + } + }); + + isLoading.value = false; + } + + /// Save / update review + Future submitReview() async { + if (comment.value.text.trim().isEmpty || ratings.value == 0) { + ShowToastDialog.showToast("Please provide rating and comment".tr); + return; + } + + ShowToastDialog.showLoader("Submit in...".tr); + + final user = await FireStoreUtils.getUserProfile(order.value?.driverId ?? ''); + + if (user != null) { + user.reviewsCount = (futureCount.value + 1).toString(); + user.reviewsSum = (futureSum.value + ratings.value.toInt()).toString(); + } + if (ratingModel.value != null && ratingModel.value!.id!.isNotEmpty) { + /// Update existing review + final updatedRating = RatingModel( + id: ratingModel.value!.id, + comment: comment.value.text, + photos: ratingModel.value?.photos ?? [], + rating: ratings.value, + orderId: ratingModel.value!.orderId, + driverId: ratingModel.value!.driverId, + customerId: ratingModel.value!.customerId, + vendorId: ratingModel.value?.vendorId, + uname: "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(updatedRating); + if (user != null) { + await FireStoreUtils.updateUser(user); + } + } else { + /// New review + final newRating = RatingModel( + id: Constant.getUuid(), + comment: comment.value.text, + photos: [], + rating: ratings.value, + orderId: order.value?.id, + driverId: order.value?.driverId.toString(), + customerId: Constant.userModel?.id, + uname: "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(newRating); + if (user != null) { + await FireStoreUtils.updateUser(user); + } + } + + ShowToastDialog.closeLoader(); + Get.back(result: true); + } + + @override + void onClose() { + comment.value.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/provider_controller.dart b/lib/controllers/provider_controller.dart new file mode 100644 index 0000000..b10e21d --- /dev/null +++ b/lib/controllers/provider_controller.dart @@ -0,0 +1,40 @@ +import 'package:customer/controllers/on_demand_home_controller.dart'; +import 'package:get/get.dart'; +import '../models/provider_serivce_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; + +class ProviderController extends GetxController { + RxList providerList = [].obs; + final Rxn userModel = Rxn(); + RxBool isLoading = true.obs; + + late final String providerId; + Rx onDemandHomeController = Get.put(OnDemandHomeController()).obs; + + @override + void onInit() { + super.onInit(); + + //Get providerId from arguments + providerId = Get.arguments['providerId']; + + getProvider(); + getAuthor(); + } + + void getProvider() async { + FireStoreUtils.getProviderServiceByProviderId(providerId: providerId).then((catValue) { + providerList.value = catValue; + }); + + isLoading.value = false; + } + + Future getAuthor() async { + final user = await FireStoreUtils.getUserProfile(providerId); + if (user != null) { + userModel.value = user; + } + } +} diff --git a/lib/controllers/rate_product_controller.dart b/lib/controllers/rate_product_controller.dart new file mode 100644 index 0000000..a31cbc6 --- /dev/null +++ b/lib/controllers/rate_product_controller.dart @@ -0,0 +1,194 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/order_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../models/rating_model.dart'; +import '../models/review_attribute_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import '../themes/show_toast_dialog.dart'; + +class RateProductController extends GetxController { + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Rx commentController = TextEditingController().obs; + + Rx orderModel = OrderModel().obs; + RxString productId = "".obs; + Rx ratingModel = RatingModel().obs; + Rx productModel = ProductModel().obs; + Rx vendorModel = VendorModel().obs; + Rx vendorCategoryModel = VendorCategoryModel().obs; + + RxList reviewAttributeList = [].obs; + + RxDouble ratings = 0.0.obs; + + RxMap reviewAttribute = {}.obs; + RxMap reviewProductAttributes = {}.obs; + + RxDouble vendorReviewSum = 0.0.obs; + RxDouble vendorReviewCount = 0.0.obs; + + RxDouble productReviewSum = 0.0.obs; + RxDouble productReviewCount = 0.0.obs; + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + orderModel.value = argumentData['orderModel']; + productId.value = argumentData['productId']; + + await FireStoreUtils.getOrderReviewsByID(orderModel.value.id.toString(), productId.value).then((value) { + if (value != null) { + ratingModel.value = value; + ratings.value = value.rating ?? 0.0; + commentController.value.text = value.comment.toString(); + reviewAttribute.value = value.reviewAttributes!; + images.addAll(value.photos ?? []); + } + }); + + await FireStoreUtils.getProductById(productId.value.split('~').first).then((value) { + if (value != null) { + productModel.value = value; + if (ratingModel.value.id != null && ratingModel.value.id!.isNotEmpty) { + productReviewCount.value = value.reviewsCount! - 1; + productReviewSum.value = value.reviewsSum! - ratings.value; + + if (value.reviewAttributes != null) { + value.reviewAttributes!.forEach((key, value) { + ReviewsAttribute reviewsAttributeModel = ReviewsAttribute.fromJson(value); + reviewsAttributeModel.reviewsCount = reviewsAttributeModel.reviewsCount! - 1; + reviewsAttributeModel.reviewsSum = reviewsAttributeModel.reviewsSum! - reviewAttribute[key]; + reviewProductAttributes.addEntries([MapEntry(key, reviewsAttributeModel.toJson())]); + }); + } + } else { + productReviewCount.value = double.parse(value.reviewsCount.toString()); + productReviewSum.value = double.parse(value.reviewsSum.toString()); + if (value.reviewAttributes != null) { + reviewProductAttributes.value = value.reviewAttributes!; + } + } + } + }); + + await FireStoreUtils.getVendorById(productModel.value.vendorID.toString()).then((value) { + if (value != null) { + vendorModel.value = value; + if (ratingModel.value.id != null && ratingModel.value.id!.isNotEmpty) { + vendorReviewCount.value = value.reviewsCount! - 1; + vendorReviewSum.value = value.reviewsSum! - ratings.value; + } else { + vendorReviewCount.value = double.parse(value.reviewsCount.toString()); + vendorReviewSum.value = double.parse(value.reviewsSum.toString()); + } + } + }); + + await FireStoreUtils.getVendorCategoryByCategoryId(productModel.value.categoryID.toString()).then((value) async { + if (value != null) { + vendorCategoryModel.value = value; + for (var element in vendorCategoryModel.value.reviewAttributes!) { + await FireStoreUtils.getVendorReviewAttribute(element).then((value) { + reviewAttributeList.add(value!); + }); + } + } + }); + } + + isLoading.value = false; + } + + Future saveRating() async { + if (ratings.value != 0.0) { + ShowToastDialog.showLoader("Please wait...".tr); + productModel.value.reviewsCount = productReviewCount.value + 1; + productModel.value.reviewsSum = productReviewSum.value + ratings.value; + productModel.value.reviewAttributes = reviewProductAttributes; + + vendorModel.value.reviewsCount = vendorReviewCount.value + 1; + vendorModel.value.reviewsSum = vendorReviewSum.value + ratings.value; + + if (reviewProductAttributes.isEmpty) { + reviewAttribute.forEach((key, value) { + ReviewsAttribute reviewsAttributeModel = ReviewsAttribute(reviewsCount: 1, reviewsSum: value); + reviewProductAttributes.addEntries([MapEntry(key, reviewsAttributeModel.toJson())]); + }); + } else { + reviewProductAttributes.forEach((key, value) { + ReviewsAttribute reviewsAttributeModel = ReviewsAttribute.fromJson(value); + reviewsAttributeModel.reviewsCount = reviewsAttributeModel.reviewsCount! + 1; + reviewsAttributeModel.reviewsSum = reviewsAttributeModel.reviewsSum! + reviewAttribute[key]; + reviewProductAttributes.addEntries([MapEntry(key, reviewsAttributeModel.toJson())]); + }); + } + + for (int i = 0; i < images.length; i++) { + if (images[i].runtimeType == XFile) { + String url = await Constant.uploadUserImageToFireStorage(File(images[i].path), "profileImage/${FireStoreUtils.getCurrentUid()}", File(images[i].path).path.split('/').last); + images.removeAt(i); + images.insert(i, url); + } + } + + RatingModel ratingProduct = RatingModel( + productId: productId.value, + comment: commentController.value.text, + photos: images, + rating: ratings.value, + customerId: FireStoreUtils.getCurrentUid(), + id: ratingModel.value.id != null && ratingModel.value.id!.isNotEmpty ? ratingModel.value.id : Constant.getUuid(), + orderId: orderModel.value.id, + vendorId: productModel.value.vendorID, + createdAt: Timestamp.now(), + uname: Constant.userModel!.fullName(), + profile: Constant.userModel!.profilePictureURL, + reviewAttributes: reviewAttribute, + ); + + print("vendor model"); + log(vendorModel.value.toJson().toString()); + await FireStoreUtils.updateReviewById(ratingProduct); + print("Rating Saved"); + print(ratingProduct.toJson()); + await FireStoreUtils.updateVendor(vendorModel.value); + await FireStoreUtils.setProduct(productModel.value); + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Rating saved successfully.".tr); + Get.back(); + } else { + ShowToastDialog.showToast("Please add rate for food item.".tr); + } + } + + final ImagePicker _imagePicker = ImagePicker(); + RxList images = [].obs; + + Future pickFile({required ImageSource source}) async { + try { + XFile? image = await _imagePicker.pickImage(source: source); + if (image == null) return; + images.add(image); + Get.back(); + } on PlatformException catch (e) { + ShowToastDialog.showToast("Failed to Pick : \n $e"); + } + } +} diff --git a/lib/controllers/redeem_gift_card_controller.dart b/lib/controllers/redeem_gift_card_controller.dart new file mode 100644 index 0000000..d445e2f --- /dev/null +++ b/lib/controllers/redeem_gift_card_controller.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class RedeemGiftCardController extends GetxController { + Rx giftCodeController = TextEditingController().obs; + Rx giftPinController = TextEditingController().obs; + + @override + void onInit() { + // TODO: implement onInit + super.onInit(); + } +} diff --git a/lib/controllers/refer_friend_controller.dart b/lib/controllers/refer_friend_controller.dart new file mode 100644 index 0000000..8b9cd87 --- /dev/null +++ b/lib/controllers/refer_friend_controller.dart @@ -0,0 +1,25 @@ +import 'package:customer/models/referral_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class ReferFriendController extends GetxController { + Rx referralModel = ReferralModel().obs; + + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + Future getData() async { + await FireStoreUtils.getReferralUserBy().then((value) { + if (value != null) { + referralModel.value = value; + } + }); + isLoading.value = false; + } +} diff --git a/lib/controllers/rental_conformation_controller.dart b/lib/controllers/rental_conformation_controller.dart new file mode 100644 index 0000000..a272942 --- /dev/null +++ b/lib/controllers/rental_conformation_controller.dart @@ -0,0 +1,89 @@ +import 'dart:math' as maths; + +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/rental_order_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../screen_ui/rental_service/rental_dashboard_screen.dart'; +import 'cab_rental_dashboard_controllers.dart'; + +class RentalConformationController extends GetxController { + RxBool isLoading = false.obs; + + Rx rentalOrderModel = RentalOrderModel().obs; + Rx couponController = TextEditingController().obs; + + @override + void onInit() { + getArguments(); + fetchCoupons(); + super.onInit(); + } + + void getArguments() { + final args = Get.arguments; + if (args.containsKey('rentalOrderModel') && args['rentalOrderModel'] is RentalOrderModel) { + rentalOrderModel.value = args['rentalOrderModel'] as RentalOrderModel; + calculateAmount(); + } else { + debugPrint('No rental order found in arguments or invalid format.'); + } + isLoading.value = false; + } + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + Rx selectedCouponModel = CouponModel().obs; + + void calculateAmount() { + subTotal.value = 0.0; + discount.value = 0.0; + taxAmount.value = 0.0; + totalAmount.value = 0.0; + + subTotal.value = double.tryParse(rentalOrderModel.value.subTotal ?? '0') ?? 0.0; + if (selectedCouponModel.value.id != null) { + discount.value = Constant.calculateDiscount(amount: subTotal.value.toString(), offerModel: selectedCouponModel.value); + } + for (var element in rentalOrderModel.value.taxSetting ?? []) { + taxAmount.value = (taxAmount.value + Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element)); + } + + totalAmount.value = subTotal.value - discount.value + taxAmount.value; + } + + RxList couponList = [].obs; + + Future fetchCoupons() async { + try { + await FireStoreUtils.getRentalCoupon().then((value) { + couponList.value = value; + }); + } catch (e) { + print("Error fetching coupons: $e"); + } + } + + Future placeOrder() async { + ShowToastDialog.showLoader("Placing booking...".tr); + rentalOrderModel.value.discount = discount.value.toString(); + rentalOrderModel.value.couponCode = selectedCouponModel.value.code; + rentalOrderModel.value.couponId = selectedCouponModel.value.id; + rentalOrderModel.value.subTotal = subTotal.value.toString(); + rentalOrderModel.value.otpCode = (maths.Random().nextInt(9000) + 1000).toString(); + await FireStoreUtils.rentalOrderPlace(rentalOrderModel.value).then((value) async { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Order placed successfully".tr); + Get.offAll(const RentalDashboardScreen()); + CabRentalDashboardControllers controller = Get.put(CabRentalDashboardControllers()); + controller.selectedIndex.value = 1; + // Get.back(); + }); + } +} diff --git a/lib/controllers/rental_coupon_controller.dart b/lib/controllers/rental_coupon_controller.dart new file mode 100644 index 0000000..9f4b303 --- /dev/null +++ b/lib/controllers/rental_coupon_controller.dart @@ -0,0 +1,28 @@ +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class RentalCouponController extends GetxController{ + + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + + void getData(){ + getCouponCode(); + } + RxBool isLoading = true.obs; + RxList cabCouponList = [].obs; + + Future getCouponCode() async { + await FireStoreUtils.getRentalCoupon().then((value) { + cabCouponList.value = value; + }); + print("cabCouponList ${cabCouponList.length}"); + isLoading.value = false; + } +} \ No newline at end of file diff --git a/lib/controllers/rental_home_controller.dart b/lib/controllers/rental_home_controller.dart new file mode 100644 index 0000000..779eea6 --- /dev/null +++ b/lib/controllers/rental_home_controller.dart @@ -0,0 +1,237 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/rental_order_model.dart'; +import 'package:customer/models/rental_package_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/rental_service/rental_conformation_screen.dart'; +import 'package:customer/widget/geoflutterfire/src/geoflutterfire.dart'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as latlong; +import '../constant/constant.dart'; +import '../models/payment_model/cod_setting_model.dart'; +import '../models/payment_model/flutter_wave_model.dart'; +import '../models/payment_model/mercado_pago_model.dart'; +import '../models/payment_model/mid_trans.dart'; +import '../models/payment_model/orange_money.dart'; +import '../models/payment_model/pay_fast_model.dart'; +import '../models/payment_model/pay_stack_model.dart'; +import '../models/payment_model/paypal_model.dart'; +import '../models/payment_model/paytm_model.dart'; +import '../models/payment_model/razorpay_model.dart'; +import '../models/payment_model/stripe_model.dart'; +import '../models/payment_model/wallet_setting_model.dart'; +import '../models/payment_model/xendit.dart'; +import '../models/rental_vehicle_type.dart'; +import '../screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; +import '../utils/preferences.dart'; +import '../utils/utils.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as gmaps; + +class RentalHomeController extends GetxController { + RxBool isLoading = false.obs; + + // Location input + final Rx sourceTextEditController = TextEditingController().obs; + + // Selected date + Rx selectedDate = DateTime.now().obs; + + // Vehicle list + selected vehicle + RxList vehicleTypes = [].obs; + Rx selectedVehicleType = Rx(null); + + RxList rentalPackages = [].obs; + Rx selectedPackage = Rx(null); + + Rx userModel = UserModel().obs; + + final RxString selectedPaymentMethod = ''.obs; + + final Rx departureLatLong = gmaps.LatLng(0.0, 0.0).obs; + final Rx departureLatLongOsm = latlong.LatLng(0.0, 0.0).obs; + + @override + void onInit() { + super.onInit(); + if (Constant.userModel != null) { + userModel.value = Constant.userModel!; + } + getVehicleType(); + fetchCurrentLocation(); + } + + void fetchCurrentLocation() async { + try { + Position? position = await Utils.getCurrentLocation(); + if (position != null) { + Constant.currentLocation = position; + + // Set default coordinates for Google or OSM + departureLatLong.value = gmaps.LatLng(position.latitude, position.longitude); + departureLatLongOsm.value = latlong.LatLng(position.latitude, position.longitude); + + // Get readable address + String address = await Utils.getAddressFromCoordinates(position.latitude, position.longitude); + sourceTextEditController.value.text = address; + } + } catch (e) { + ShowToastDialog.showToast("Unable to fetch current location".tr); + } + } + + /// Fetch Vehicle Types + Future getVehicleType() async { + isLoading.value = true; + await FireStoreUtils.getRentalVehicleType().then((value) async { + vehicleTypes.value = value; + if (vehicleTypes.isNotEmpty) { + selectedVehicleType.value = vehicleTypes[0]; + await getRentalPackage(); + } + }); + await getPaymentSettings(); + isLoading.value = false; + } + + /// Date Picker + Future pickDate(BuildContext context) async { + final DateTime? picked = await showDatePicker(context: context, initialDate: selectedDate.value, firstDate: DateTime.now(), lastDate: DateTime(2100)); + + if (picked != null) { + selectedDate.value = picked; + } + } + + Future getRentalPackage() async { + await FireStoreUtils.getRentalPackage(selectedVehicleType.value!.id.toString()).then((value) { + rentalPackages.value = value; + if (rentalPackages.isNotEmpty) { + selectedPackage.value = rentalPackages[0]; + } + }); + } + + void completeOrder() { + DestinationLocation sourceLocation = DestinationLocation( + latitude: Constant.selectedMapType == 'osm' ? departureLatLongOsm.value.latitude : departureLatLong.value.latitude, + longitude: Constant.selectedMapType == 'osm' ? departureLatLongOsm.value.longitude : departureLatLong.value.longitude, + ); + + print("=====>"); + print(sourceTextEditController.value.text); + + RentalOrderModel rentalOrderModel = RentalOrderModel(); + rentalOrderModel.id = Constant.getUuid(); + rentalOrderModel.authorID = userModel.value.id; + rentalOrderModel.author = userModel.value; + rentalOrderModel.rentalVehicleType = selectedVehicleType.value; + rentalOrderModel.vehicleId = selectedVehicleType.value!.id; + rentalOrderModel.sectionId = Constant.sectionConstantModel!.id; + rentalOrderModel.sourceLocationName = sourceTextEditController.value.text; + rentalOrderModel.bookingDateTime = Timestamp.fromDate(selectedDate.value); + rentalOrderModel.paymentMethod = selectedPaymentMethod.value; + rentalOrderModel.paymentStatus = false; + rentalOrderModel.status = Constant.orderPlaced; + rentalOrderModel.subTotal = selectedPackage.value!.baseFare; + rentalOrderModel.rentalPackageModel = selectedPackage.value; + rentalOrderModel.taxSetting = Constant.taxList; + rentalOrderModel.createdAt = Timestamp.now(); + rentalOrderModel.sourceLocation = sourceLocation; + rentalOrderModel.adminCommission = Constant.sectionConstantModel!.adminCommision!.amount; + rentalOrderModel.adminCommissionType = Constant.sectionConstantModel!.adminCommision!.commissionType; + rentalOrderModel.sourcePoint = G( + geopoint: GeoPoint(sourceLocation.latitude ?? 0.0, sourceLocation.longitude ?? 0.0), + geohash: Geoflutterfire().point(latitude: sourceLocation.latitude ?? 0.0, longitude: sourceLocation.longitude ?? 0.0).hash, + ); + rentalOrderModel.zoneId = Constant.getZoneId(sourceLocation.latitude ?? 0.0, sourceLocation.longitude ?? 0.0); + log(rentalOrderModel.toJson().toString()); + Get.back(); + Get.back(); + + Get.to(() => RentalConformationScreen(), arguments: {"rentalOrderModel": rentalOrderModel}); + } + + void setDepartureMarker(double lat, double lng) { + if (Constant.selectedMapType == 'osm') { + departureLatLongOsm.value = latlong.LatLng(lat, lng); + } else { + departureLatLong.value = gmaps.LatLng(lat, lng); + } + } + + // final Rx departureLatLong = const LatLng(0.0, 0.0).obs; + // final Rx departureLatLongOsm = latlong.LatLng(0.0, 0.0).obs; + + // void setDepartureMarker(double lat, double long) { + // if (Constant.selectedMapType == 'osm') { + // departureLatLongOsm.value = latlong.LatLng(lat, long); + // } else { + // departureLatLong.value = LatLng(lat, long); + // } + // } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + }); + } +} diff --git a/lib/controllers/rental_order_details_controller.dart b/lib/controllers/rental_order_details_controller.dart new file mode 100644 index 0000000..59bd3e6 --- /dev/null +++ b/lib/controllers/rental_order_details_controller.dart @@ -0,0 +1,904 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as maths; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/payment_model/cod_setting_model.dart'; +import 'package:customer/models/payment_model/flutter_wave_model.dart'; +import 'package:customer/models/payment_model/mercado_pago_model.dart'; +import 'package:customer/models/payment_model/mid_trans.dart'; +import 'package:customer/models/payment_model/orange_money.dart'; +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:customer/models/payment_model/pay_stack_model.dart'; +import 'package:customer/models/payment_model/paypal_model.dart'; +import 'package:customer/models/payment_model/paytm_model.dart'; +import 'package:customer/models/payment_model/razorpay_model.dart'; +import 'package:customer/models/payment_model/stripe_model.dart'; +import 'package:customer/models/payment_model/wallet_setting_model.dart'; +import 'package:customer/models/payment_model/xendit.dart'; +import 'package:customer/models/rating_model.dart'; +import 'package:customer/models/rental_order_model.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/payment/MercadoPagoScreen.dart'; +import 'package:customer/payment/PayFastScreen.dart'; +import 'package:customer/payment/getPaytmTxtToken.dart'; +import 'package:customer/payment/midtrans_screen.dart'; +import 'package:customer/payment/orangePayScreen.dart'; +import 'package:customer/payment/paystack/pay_stack_screen.dart'; +import 'package:customer/payment/paystack/pay_stack_url_model.dart'; +import 'package:customer/payment/paystack/paystack_url_genrater.dart'; +import 'package:customer/payment/stripe_failed_model.dart'; +import 'package:customer/payment/xenditModel.dart'; +import 'package:customer/payment/xenditScreen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:uuid/uuid.dart'; + +import '../constant/constant.dart'; +import '../models/tax_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; + +class RentalOrderDetailsController extends GetxController { + Rx order = RentalOrderModel().obs; + RxBool isLoading = true.obs; + + Rx driverUser = Rx(null); + + RxDouble subTotal = 0.0.obs; + RxDouble discount = 0.0.obs; + RxDouble taxAmount = 0.0.obs; + RxDouble totalAmount = 0.0.obs; + RxDouble extraKilometerCharge = 0.0.obs; + RxDouble extraMinutesCharge = 0.0.obs; + + final RxString selectedPaymentMethod = ''.obs; + Rx ratingModel = RatingModel().obs; + + @override + void onInit() { + getData(); + super.onInit(); + } + + Future getData() async { + final args = Get.arguments; + if (args != null) { + order.value = args as RentalOrderModel; + calculateTotalAmount(); + await fetchDriverDetails(); + await getPaymentSettings(); + } + isLoading.value = false; + } + + Future fetchDriverDetails() async { + if (order.value.driverId != null) { + await FireStoreUtils.getUserProfile(order.value.driverId ?? '').then((value) { + if (value != null) { + driverUser.value = value; + } + }); + + await FireStoreUtils.getReviewsbyID(order.value.id.toString()).then((value) { + if (value != null) { + ratingModel.value = value; + } + }); + } + } + + String getExtraKm() { + try { + final double start = double.tryParse(order.value.startKitoMetersReading ?? '0') ?? 0.0; + final double end = double.tryParse(order.value.endKitoMetersReading ?? '0') ?? 0.0; + final double included = double.tryParse(order.value.rentalPackageModel?.includedDistance?.toString() ?? '0') ?? 0.0; + + // Calculate extra km safely + final double extra = (end - start - included); + final double validExtra = extra > 0 ? extra : 0; + + return "${validExtra.toStringAsFixed(2)} ${Constant.distanceType}"; + } catch (e) { + return "0 ${Constant.distanceType}"; + } + } + + ///Safe calculation after order is loaded + void calculateTotalAmount() { + try { + subTotal.value = double.tryParse(order.value.subTotal?.toString() ?? "0") ?? 0.0; + discount.value = double.tryParse(order.value.discount?.toString() ?? "0") ?? 0.0; + taxAmount.value = 0.0; + + if (order.value.endTime != null) { + DateTime start = order.value.startTime!.toDate(); + DateTime end = order.value.endTime!.toDate(); + + // Total rented minutes + int totalMinutes = end.difference(start).inMinutes; + + int includedMinutes = (int.tryParse(order.value.rentalPackageModel?.includedHours.toString() ?? "0") ?? 0) * 60; + + if (totalMinutes > includedMinutes) { + int extraMinutes = totalMinutes - includedMinutes; + + double minuteFare = double.tryParse(order.value.rentalPackageModel?.extraMinuteFare?.toString() ?? "0") ?? 0.0; + + extraMinutesCharge.value = extraMinutes * minuteFare; + } else { + extraMinutesCharge.value = 0; + } + } + + if (order.value.startKitoMetersReading != null && order.value.endKitoMetersReading != null) { + double startKm = double.tryParse(order.value.startKitoMetersReading?.toString() ?? "0") ?? 0.0; + double endKm = double.tryParse(order.value.endKitoMetersReading?.toString() ?? "0") ?? 0.0; + if (endKm > startKm) { + double totalKm = endKm - startKm; + if (totalKm > double.parse(order.value.rentalPackageModel!.includedDistance!)) { + totalKm = totalKm - double.parse(order.value.rentalPackageModel!.includedDistance!); + double extraKmRate = double.tryParse(order.value.rentalPackageModel?.extraKmFare?.toString() ?? "0") ?? 0.0; + extraKilometerCharge.value = totalKm * extraKmRate; + } + } + } + + subTotal.value = subTotal.value + extraKilometerCharge.value + extraMinutesCharge.value; + + if (order.value.taxSetting != null) { + for (var element in order.value.taxSetting!) { + taxAmount.value += Constant.calculateTax(amount: (subTotal.value - discount.value).toString(), taxModel: element); + } + } + + totalAmount.value = (subTotal.value - discount.value) + taxAmount.value; + } catch (e) { + ShowToastDialog.showToast("${'Failed to calculate total:'.tr} $e"); + } + } + + Future completeOrder() async { + if (selectedPaymentMethod.value == PaymentGateway.cod.name) { + order.value.paymentMethod = selectedPaymentMethod.value; + await FireStoreUtils.rentalOrderPlace(order.value).then((value) { + ShowToastDialog.showToast("Payment method changed".tr); + Get.back(); + Get.back(); + }); + } else { + order.value.paymentStatus = true; + order.value.paymentMethod = selectedPaymentMethod.value; + if (selectedPaymentMethod.value == PaymentGateway.wallet.name) { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(totalAmount.toString()), + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: false, + orderId: order.value.id, + note: "Rental Amount debited", + paymentStatus: "success", + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: "-${totalAmount.toString()}", userId: FireStoreUtils.getCurrentUid()); + } + }); + } + + await FireStoreUtils.rentalOrderPlace(order.value).then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + Get.back(); + Get.back(); + }); + } + } + + Future cancelRentalRequest(RentalOrderModel order, {List? taxList}) async { + try { + isLoading.value = true; + + order.status = Constant.orderCancelled; + await FireStoreUtils.rentalOrderPlace(order); + + if (order.paymentMethod?.toLowerCase() != "cod") { + double totalTax = 0.0; + + if (taxList != null) { + for (var element in taxList) { + totalTax += Constant.calculateTax(amount: (double.parse(order.subTotal.toString()) - double.parse(order.discount.toString())).toString(), taxModel: element); + } + } + + double subTotal = double.parse(order.subTotal.toString()) - double.parse(order.discount.toString()); + double refundAmount = subTotal + totalTax; + + WalletTransactionModel walletTransaction = WalletTransactionModel( + id: Constant.getUuid(), + amount: refundAmount, + date: Timestamp.now(), + paymentMethod: PaymentGateway.wallet.name, + transactionUser: "customer", + userId: FireStoreUtils.getCurrentUid(), + isTopup: true, + // refund + orderId: order.id, + note: "Refund for cancelled booking", + paymentStatus: "success", + serviceType: Constant.parcelServiceType, + ); + + await FireStoreUtils.setWalletTransaction(walletTransaction); + await FireStoreUtils.updateUserWallet(amount: refundAmount.toString(), userId: FireStoreUtils.getCurrentUid()); + } + ShowToastDialog.showToast("Booking cancelled successfully".tr); + Get.back(); + } catch (e) { + ShowToastDialog.showToast("${'Failed to cancel booking:'.tr} $e".tr); + } finally { + isLoading.value = false; + } + } + + Rx walletSettingModel = WalletSettingModel().obs; + Rx cashOnDeliverySettingModel = CodSettingModel().obs; + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx paytmModel = PaytmModel().obs; + Rx razorPayModel = RazorPayModel().obs; + + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + paytmModel.value = PaytmModel.fromJson(jsonDecode(Preferences.getString(Preferences.paytmSettings))); + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(jsonDecode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + walletSettingModel.value = WalletSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.walletSettings))); + cashOnDeliverySettingModel.value = CodSettingModel.fromJson(jsonDecode(Preferences.getString(Preferences.codSettings))); + + if (walletSettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.wallet.name; + } else if (cashOnDeliverySettingModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.cod.name; + } else if (stripeModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.stripe.name; + } else if (payPalModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.paypal.name; + } else if (payStackModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payStack.name; + } else if (mercadoPagoModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.mercadoPago.name; + } else if (flutterWaveModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.flutterWave.name; + } else if (payFastModel.value.isEnable == true) { + selectedPaymentMethod.value = PaymentGateway.payFast.name; + } else if (razorPayModel.value.isEnabled == true) { + selectedPaymentMethod.value = PaymentGateway.razorpay.name; + } else if (midTransModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.midTrans.name; + } else if (orangeMoneyModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.orangeMoney.name; + } else if (xenditModel.value.enable == true) { + selectedPaymentMethod.value = PaymentGateway.xendit.name; + } + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'eMart Customer'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": Constant.userModel!.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": Constant.userModel!.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", + // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Error creating preference: ${response.body}'); + return null; + } + } + + //Paypal + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + completeOrder(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen( + amount: (double.parse(totalAmount) * 100).toString(), + currency: "ZAR", + secretKey: payStackModel.value.secretKey.toString(), + userModel: Constant.userModel!, + ).then((value) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": Constant.userModel!.email.toString(), + "phonenumber": Constant.userModel!.phoneNumber, // Add a real phone number + "name": Constant.userModel!.fullName(), // Add a real customer name + }, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: Constant.userModel!).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + completeOrder(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///Paytm payment function + Future getPaytmCheckSum(context, {required double amount}) async { + // final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + // String getChecksum = "${Constant.globalUrl}payments/getpaytmchecksum"; + // + // final response = await http.post( + // Uri.parse(getChecksum), + // headers: {}, + // body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString()}, + // ); + // + // final data = jsonDecode(response.body); + // await verifyCheckSum(checkSum: data["code"], amount: amount, orderId: orderId).then((value) { + // initiatePayment(amount: amount, orderId: orderId).then((value) { + // String callback = ""; + // if (paytmModel.value.isSandboxEnabled == true) { + // callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } else { + // callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + // } + // + // GetPaymentTxtTokenModel result = value; + // startTransaction(context, txnTokenBy: result.body.txnToken, orderId: orderId, amount: amount, callBackURL: callback, isStaging: paytmModel.value.isSandboxEnabled); + // }); + // }); + } + + Future startTransaction(context, {required String txnTokenBy, required orderId, required double amount, required callBackURL, required isStaging}) async { + // try { + // var response = AllInOneSdk.startTransaction( + // paytmModel.value.paytmMID.toString(), + // orderId, + // amount.toString(), + // txnTokenBy, + // callBackURL, + // isStaging, + // true, + // true, + // ); + // + // response.then((value) { + // if (value!["RESPMSG"] == "Txn Success") { + // print("txt done!!"); + // ShowToastDialog.showToast("Payment Successful!!"); + // completeOrder(); + // } + // }).catchError((onError) { + // if (onError is PlatformException) { + // Get.back(); + // + // ShowToastDialog.showToast(onError.message.toString()); + // } else { + // log("======>>2"); + // Get.back(); + // ShowToastDialog.showToast(onError.message.toString()); + // } + // }); + // } catch (err) { + // Get.back(); + // ShowToastDialog.showToast(err.toString()); + // } + } + + Future verifyCheckSum({required String checkSum, required double amount, required orderId}) async { + String getChecksum = "${Constant.globalUrl}payments/validatechecksum"; + final response = await http.post( + Uri.parse(getChecksum), + headers: {}, + body: {"mid": paytmModel.value.paytmMID.toString(), "order_id": orderId, "key_secret": paytmModel.value.pAYTMMERCHANTKEY.toString(), "checksum_value": checkSum}, + ); + final data = jsonDecode(response.body); + return data['status']; + } + + Future initiatePayment({required double amount, required orderId}) async { + String initiateURL = "${Constant.globalUrl}payments/initiatepaytmpayment"; + String callback = ""; + if (paytmModel.value.isSandboxEnabled == true) { + callback = "${callback}https://securegw-stage.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } else { + callback = "${callback}https://securegw.paytm.in/theia/paytmCallback?ORDER_ID=$orderId"; + } + final response = await http.post( + Uri.parse(initiateURL), + headers: {}, + body: { + "mid": paytmModel.value.paytmMID, + "order_id": orderId, + "key_secret": paytmModel.value.pAYTMMERCHANTKEY, + "amount": amount.toString(), + "currency": "INR", + "callback_url": callback, + "custId": FireStoreUtils.getCurrentUid(), + "issandbox": paytmModel.value.isSandboxEnabled == true ? "1" : "2", + }, + ); + log(response.body); + final data = jsonDecode(response.body); + if (data["body"]["txnToken"] == null || data["body"]["txnToken"].toString().isEmpty) { + Get.back(); + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + } + return GetPaymentTxtTokenModel.fromJson(data); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': Constant.userModel!.phoneNumber, 'email': Constant.userModel!.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + final currentDate = DateTime.now(); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + (); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + // Handle the response + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + // ignore: use_build_context_synchronously + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + completeOrder(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/controllers/rental_review_controller.dart b/lib/controllers/rental_review_controller.dart new file mode 100644 index 0000000..cc40f11 --- /dev/null +++ b/lib/controllers/rental_review_controller.dart @@ -0,0 +1,137 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../constant/collection_name.dart'; +import '../models/rating_model.dart'; +import '../models/rental_order_model.dart'; +import '../models/user_model.dart'; +import '../service/fire_store_utils.dart'; +import '../constant/constant.dart'; +import '../themes/show_toast_dialog.dart'; + +class RentalReviewController extends GetxController { + RxBool isLoading = true.obs; + + /// Order from arguments + final Rx order = Rx(null); + + /// Rating data + final Rx ratingModel = Rx(null); + final RxDouble ratings = 0.0.obs; + final Rx comment = TextEditingController().obs; + + /// Driver (to be reviewed) + final Rx driverUser = Rx(null); + + /// Review stats + final RxInt futureCount = 0.obs; + final RxInt futureSum = 0.obs; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + if (args != null && args['order'] != null) { + order.value = args['order'] as RentalOrderModel; + getReview(); + } + } + + /// Fetch old review + driver stats + Future getReview() async { + await FireStoreUtils.getReviewsbyID(order.value?.id ?? "").then((value) { + if (value != null) { + ratingModel.value = value; + ratings.value = value.rating ?? 0; + comment.value.text = value.comment ?? ""; + } + }); + + await FireStoreUtils.getUserProfile(order.value?.driverId ?? '').then((value) { + if (value != null) { + driverUser.value = value; + + final int userReviewsCount = int.tryParse(driverUser.value!.reviewsCount?.toString() ?? "0") ?? 0; + final int userReviewsSum = int.tryParse(driverUser.value!.reviewsSum?.toString() ?? "0") ?? 0; + + if (ratingModel.value != null) { + final int oldRating = ratingModel.value?.rating?.toInt() ?? 0; + futureCount.value = userReviewsCount - 1; + futureSum.value = userReviewsSum - oldRating; + } else { + futureCount.value = userReviewsCount; + futureSum.value = userReviewsSum; + } + } + }); + + isLoading.value = false; + } + + /// Save / update review + Future submitReview() async { + if (comment.value.text.trim().isEmpty || ratings.value == 0) { + ShowToastDialog.showToast("Please provide rating and comment".tr); + return; + } + + ShowToastDialog.showLoader("Submit in...".tr); + + final user = await FireStoreUtils.getUserProfile(order.value?.driverId ?? ''); + + if (user != null) { + user.reviewsCount = (futureCount.value + 1).toString(); + user.reviewsSum = (futureSum.value + ratings.value.toInt()).toString(); + } + if (ratingModel.value != null) { + /// Update existing review + final updatedRating = RatingModel( + id: ratingModel.value!.id, + comment: comment.value.text, + photos: ratingModel.value?.photos ?? [], + rating: ratings.value, + orderId: ratingModel.value!.orderId, + driverId: ratingModel.value!.driverId, + customerId: ratingModel.value!.customerId, + vendorId: ratingModel.value?.vendorId, + uname: "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(updatedRating); + if (user != null) { + await FireStoreUtils.updateUser(user); + } + } else { + /// New review + final docRef = FireStoreUtils.fireStore.collection(CollectionName.itemsReview).doc(); + final newRating = RatingModel( + id: docRef.id, + comment: comment.value.text, + photos: [], + rating: ratings.value, + orderId: order.value?.id, + driverId: order.value?.driverId.toString(), + customerId: Constant.userModel?.id, + uname: "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + profile: Constant.userModel?.profilePictureURL, + createdAt: Timestamp.now(), + ); + + await FireStoreUtils.updateReviewById(newRating); + if (user != null) { + await FireStoreUtils.updateUser(user); + } + } + + ShowToastDialog.closeLoader(); + Get.back(result: true); + } + + @override + void onClose() { + comment.value.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/restaurant_details_controller.dart b/lib/controllers/restaurant_details_controller.dart new file mode 100644 index 0000000..7901954 --- /dev/null +++ b/lib/controllers/restaurant_details_controller.dart @@ -0,0 +1,300 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/brands_model.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/favourite_item_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../models/attributes_model.dart'; +import '../service/cart_provider.dart'; +import '../service/fire_store_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +class RestaurantDetailsController extends GetxController { + Rx searchEditingController = TextEditingController().obs; + + RxBool isLoading = true.obs; + Rx pageController = PageController().obs; + RxInt currentPage = 0.obs; + + RxBool isVag = false.obs; + RxBool isNonVag = false.obs; + RxBool isMenuOpen = false.obs; + + RxList favouriteList = [].obs; + RxList favouriteItemList = [].obs; + RxList allProductList = [].obs; + RxList productList = [].obs; + RxList vendorCategoryList = [].obs; + + RxList couponList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + + super.onInit(); + } + + void animateSlider() { + if (vendorModel.value.photos != null && vendorModel.value.photos!.isNotEmpty) { + Timer.periodic(const Duration(seconds: 2), (Timer timer) { + if (currentPage < vendorModel.value.photos!.length - 1) { + currentPage++; + } else { + currentPage.value = 0; + } + + if (pageController.value.hasClients) { + pageController.value.animateToPage(currentPage.value, duration: const Duration(milliseconds: 300), curve: Curves.easeIn); + } + }); + } + } + + Rx vendorModel = VendorModel().obs; + + final CartProvider cartProvider = CartProvider(); + + Future getArgument() async { + cartProvider.cartStream.listen((event) async { + cartItem.clear(); + cartItem.addAll(event); + }); + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorModel.value = argumentData['vendorModel']; + } + animateSlider(); + statusCheck(); + + await getProduct(); + + isLoading.value = false; + await getFavouriteList(); + + update(); + } + + RxList brandList = [].obs; + + Future getProduct() async { + await FireStoreUtils.getProductByVendorId(vendorModel.value.id.toString()).then((value) { + if ((Constant.isSubscriptionModelApplied == true || vendorModel.value.adminCommission?.isEnabled == true) && vendorModel.value.subscriptionPlan != null) { + if (vendorModel.value.subscriptionPlan?.itemLimit == '-1') { + allProductList.value = value; + productList.value = value; + } else { + int selectedProduct = + value.length < int.parse(vendorModel.value.subscriptionPlan?.itemLimit ?? '0') ? (value.isEmpty ? 0 : (value.length)) : int.parse(vendorModel.value.subscriptionPlan?.itemLimit ?? '0'); + allProductList.value = value.sublist(0, selectedProduct); + productList.value = value.sublist(0, selectedProduct); + } + } else { + allProductList.value = value; + productList.value = value; + } + }); + + for (var element in productList) { + await FireStoreUtils.getVendorCategoryById(element.categoryID.toString()).then((value) { + if (value != null) { + vendorCategoryList.add(value); + } + }); + } + + await FireStoreUtils.getBrandList().then((value) { + brandList.value = value; + }); + + var seen = {}; + vendorCategoryList.value = vendorCategoryList.where((element) => seen.add(element.id.toString())).toList(); + } + + void searchProduct(String name) { + if (name.isEmpty) { + productList.clear(); + productList.addAll(allProductList); + } else { + isVag.value = false; + isNonVag.value = false; + productList.value = allProductList.where((p0) => p0.name!.toLowerCase().contains(name.toLowerCase())).toList(); + } + update(); + } + + void filterRecord() { + if (isVag.value == true && isNonVag.value == true) { + productList.value = allProductList.where((p0) => p0.nonveg == true || p0.nonveg == false).toList(); + } else if (isVag.value == true && isNonVag.value == false) { + productList.value = allProductList.where((p0) => p0.nonveg == false).toList(); + } else if (isVag.value == false && isNonVag.value == true) { + productList.value = allProductList.where((p0) => p0.nonveg == true).toList(); + } else if (isVag.value == false && isNonVag.value == false) { + productList.value = allProductList.where((p0) => p0.nonveg == true || p0.nonveg == false).toList(); + } + } + + Future> getProductByCategory(VendorCategoryModel vendorCategoryModel) async { + return productList.where((p0) => p0.categoryID == vendorCategoryModel.id).toList(); + } + + Future getFavouriteList() async { + if (Constant.userModel != null) { + await FireStoreUtils.getFavouriteRestaurant().then((value) { + favouriteList.value = value; + }); + + await FireStoreUtils.getFavouriteItem().then((value) { + favouriteItemList.value = value; + }); + + await FireStoreUtils.getOfferByVendorId(vendorModel.value.id.toString()).then((value) { + couponList.value = value; + }); + } + await getAttributeData(); + update(); + } + + RxBool isOpen = false.obs; + + void statusCheck() { + final now = DateTime.now(); + var day = DateFormat('EEEE', 'en_US').format(now); + var date = DateFormat('dd-MM-yyyy').format(now); + for (var element in vendorModel.value.workingHours ?? []) { + if (day == element.day.toString()) { + if (element.timeslot!.isNotEmpty) { + for (var element in element.timeslot!) { + var start = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.from}"); + var end = DateFormat("dd-MM-yyyy HH:mm").parse("$date ${element.to}"); + if (isCurrentDateInRange(start, end)) { + isOpen.value = true; + } + } + } + } + } + } + + String getBrandName(String brandId) { + String brandName = ''; + for (var element in brandList) { + if (element.id == brandId) { + brandName = element.title ?? ''; + } + } + return brandName; + } + + bool isCurrentDateInRange(DateTime startDate, DateTime endDate) { + print(startDate); + print(endDate); + final currentDate = DateTime.now(); + print(currentDate); + return currentDate.isAfter(startDate) && currentDate.isBefore(endDate); + } + + RxList attributesList = [].obs; + RxList selectedVariants = [].obs; + RxList selectedIndexVariants = [].obs; + RxList selectedIndexArray = [].obs; + + RxList selectedAddOns = [].obs; + + RxInt quantity = 1.obs; + + String calculatePrice(ProductModel productModel) { + String mainPrice = "0"; + String variantPrice = "0"; + String adOnsPrice = "0"; + + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + variantPrice = Constant.productCommissionPrice( + vendorModel.value, + productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0', + ); + } + } else { + String price = Constant.productCommissionPrice(vendorModel.value, productModel.price.toString()); + String disPrice = double.parse(productModel.disPrice.toString()) <= 0 ? "0" : Constant.productCommissionPrice(vendorModel.value, productModel.disPrice.toString()); + if (double.parse(disPrice) <= 0) { + variantPrice = price; + } else { + variantPrice = disPrice; + } + } + + for (int i = 0; i < productModel.addOnsPrice!.length; i++) { + if (selectedAddOns.contains(productModel.addOnsTitle![i]) == true) { + adOnsPrice = (double.parse(adOnsPrice.toString()) + double.parse(Constant.productCommissionPrice(vendorModel.value, productModel.addOnsPrice![i].toString()))).toString(); + } + } + adOnsPrice = (quantity.value * double.parse(adOnsPrice)).toString(); + mainPrice = ((double.parse(variantPrice.toString()) * double.parse(quantity.value.toString())) + double.parse(adOnsPrice.toString())).toString(); + return mainPrice; + } + + Future getAttributeData() async { + await FireStoreUtils.getAttributes().then((value) { + if (value != null) { + attributesList.value = value; + } + }); + } + + Future addToCart({required ProductModel productModel, required String price, required String discountPrice, required bool isIncrement, required int quantity, VariantInfo? variantInfo}) async { + CartProductModel cartProductModel = CartProductModel(); + + String adOnsPrice = "0"; + for (int i = 0; i < productModel.addOnsPrice!.length; i++) { + if (selectedAddOns.contains(productModel.addOnsTitle![i]) == true && productModel.addOnsPrice![i] != '0') { + adOnsPrice = (double.parse(adOnsPrice.toString()) + double.parse(Constant.productCommissionPrice(vendorModel.value, productModel.addOnsPrice![i].toString()))).toString(); + } + } + + if (variantInfo != null) { + cartProductModel.id = "${productModel.id!}~${variantInfo.variantId.toString()}"; + cartProductModel.name = productModel.name!; + cartProductModel.photo = productModel.photo!; + cartProductModel.categoryId = productModel.categoryID!; + cartProductModel.price = price; + cartProductModel.discountPrice = discountPrice; + cartProductModel.vendorID = vendorModel.value.id; + cartProductModel.quantity = quantity; + cartProductModel.variantInfo = variantInfo; + cartProductModel.extrasPrice = adOnsPrice; + cartProductModel.extras = selectedAddOns.isEmpty ? [] : selectedAddOns; + } else { + cartProductModel.id = productModel.id!; + cartProductModel.name = productModel.name!; + cartProductModel.photo = productModel.photo!; + cartProductModel.categoryId = productModel.categoryID!; + cartProductModel.price = price; + cartProductModel.discountPrice = discountPrice; + cartProductModel.vendorID = vendorModel.value.id; + cartProductModel.quantity = quantity; + cartProductModel.variantInfo = VariantInfo(); + cartProductModel.extrasPrice = adOnsPrice; + cartProductModel.extras = selectedAddOns.isEmpty ? [] : selectedAddOns; + } + + if (isIncrement) { + await cartProvider.addToCart(Get.context!, cartProductModel, quantity); + } else { + await cartProvider.removeFromCart(cartProductModel, quantity); + } + log("===> new ${cartItem.length}"); + update(); + } +} diff --git a/lib/controllers/restaurant_list_controller.dart b/lib/controllers/restaurant_list_controller.dart new file mode 100644 index 0000000..fe953e2 --- /dev/null +++ b/lib/controllers/restaurant_list_controller.dart @@ -0,0 +1,49 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class RestaurantListController extends GetxController { + RxBool isLoading = true.obs; + RxList vendorList = [].obs; + RxList vendorSearchList = [].obs; + + RxString title = "Restaurants".obs; + + RxList favouriteList = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorList.value = argumentData['vendorList']; + vendorSearchList.value = argumentData['vendorList']; + title.value = argumentData['title'] ?? "Restaurants"; + } + + await getFavouriteRestaurant(); + + isLoading.value = false; + } + + Future getFavouriteRestaurant() async { + if (Constant.userModel != null) { + await FireStoreUtils.getFavouriteRestaurant().then((value) { + favouriteList.value = value; + }); + } + } + + @override + void dispose() { + vendorSearchList.clear(); + super.dispose(); + } +} diff --git a/lib/controllers/review_list_controller.dart b/lib/controllers/review_list_controller.dart new file mode 100644 index 0000000..540165c --- /dev/null +++ b/lib/controllers/review_list_controller.dart @@ -0,0 +1,36 @@ +import 'package:customer/models/rating_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class ReviewListController extends GetxController { + RxBool isLoading = true.obs; + + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + Rx vendorModel = VendorModel().obs; + RxList ratingList = [].obs; + + void getArgument() { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorModel.value = argumentData['vendorModel']; + getAllReview(); + } + isLoading.value = false; + } + + Future getAllReview() async { + await FireStoreUtils.getVendorReviews(vendorModel.value.id.toString()).then( + (value) { + ratingList.value = value; + }, + ); + update(); + } +} diff --git a/lib/controllers/scan_qr_code_controller.dart b/lib/controllers/scan_qr_code_controller.dart new file mode 100644 index 0000000..066dcee --- /dev/null +++ b/lib/controllers/scan_qr_code_controller.dart @@ -0,0 +1,26 @@ +import 'package:customer/models/vendor_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class ScanQrCodeController extends GetxController { + @override + void onInit() { + // TODO: implement onInit + getData(); + super.onInit(); + } + + RxList allNearestRestaurant = [].obs; + + void getData() { + FireStoreUtils.getAllNearestRestaurant().listen((event) async { + allNearestRestaurant.addAll(event); + }); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } +} diff --git a/lib/controllers/search_controller.dart b/lib/controllers/search_controller.dart new file mode 100644 index 0000000..bf20ea6 --- /dev/null +++ b/lib/controllers/search_controller.dart @@ -0,0 +1,72 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class SearchScreenController extends GetxController { + @override + void onInit() { + // TODO: implement onInit + getArgument(); + super.onInit(); + } + + RxBool isLoading = true.obs; + RxList vendorList = [].obs; + RxList vendorSearchList = [].obs; + + RxList productList = [].obs; + RxList productSearchList = [].obs; + + Future getArgument() async { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + vendorList.value = argumentData['vendorList']; + productList.clear(); + } + isLoading.value = false; + + for (var element in vendorList) { + await FireStoreUtils.getProductByVendorId(element.id.toString()).then((value) { + if ((Constant.isSubscriptionModelApplied == true || element.adminCommission?.isEnabled == true) && element.subscriptionPlan != null) { + if (element.subscriptionPlan?.itemLimit == '-1') { + productList.addAll(value); + } else { + int selectedProduct = + value.length < int.parse(element.subscriptionPlan?.itemLimit ?? '0') ? (value.isEmpty ? 0 : (value.length)) : int.parse(element.subscriptionPlan?.itemLimit ?? '0'); + productList.addAll(value.sublist(0, selectedProduct)); + } + } else { + productList.addAll(value); + } + }); + } + } + + void onSearchTextChanged(String text) { + if (text.isEmpty) { + return; + } + vendorSearchList.clear(); + productSearchList.clear(); + for (var element in vendorList) { + if (element.title!.toLowerCase().contains(text.toLowerCase())) { + vendorSearchList.add(element); + } + } + + for (var element in productList) { + if (element.name!.toLowerCase().contains(text.toLowerCase())) { + productSearchList.add(element); + } + } + } + + @override + void dispose() { + vendorSearchList.clear(); + productSearchList.clear(); + super.dispose(); + } +} diff --git a/lib/controllers/service_list_controller.dart b/lib/controllers/service_list_controller.dart new file mode 100644 index 0000000..52cf118 --- /dev/null +++ b/lib/controllers/service_list_controller.dart @@ -0,0 +1,180 @@ +import 'package:customer/models/section_model.dart'; +import 'package:customer/screen_ui/cab_service_screens/cab_dashboard_screen.dart'; +import 'package:customer/screen_ui/ecommarce/dash_board_e_commerce_screen.dart'; +import 'package:customer/screen_ui/parcel_service/parcel_dashboard_screen.dart'; +import 'package:customer/screen_ui/rental_service/rental_dashboard_screen.dart'; +import 'package:customer/service/cart_provider.dart'; +import 'package:customer/service/database_helper.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/currency_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:firebase_auth/firebase_auth.dart' as auth; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../screen_ui/auth_screens/login_screen.dart'; +import '../screen_ui/multi_vendor_service/dash_board_screens/dash_board_screen.dart'; +import '../screen_ui/on_demand_service/on_demand_dashboard_screen.dart'; +import '../service/notification_service.dart'; + +class ServiceListController extends GetxController { + var isLoading = false.obs; + var serviceListBanner = [].obs; + var sectionList = [].obs; + var currencyData = CurrencyModel().obs; + + @override + void onInit() { + super.onInit(); + loadData(); + } + + Future loadData() async { + isLoading.value = true; + + // fetch currency + CurrencyModel? currency = await FireStoreUtils.getCurrency(); + + currencyData.value = currency ?? CurrencyModel(id: "", code: "USD", decimal: 2, isactive: true, name: "US Dollar", symbol: "\$", symbolatright: false); + + // Load sections + List sections = await FireStoreUtils.getSections(); + sectionList.assignAll(sections); + + await FireStoreUtils.getSectionBannerList().then((value) { + serviceListBanner.assignAll(value); + }); + await getZone(); + isLoading.value = false; + } + + Future getZone() async { + await FireStoreUtils.getZone().then((value) { + if (value != null) { + Constant.zoneList = value; + } + }); + } + + Future onServiceTap(BuildContext context, SectionModel sectionModel) async { + try { + ShowToastDialog.showLoader("Please wait...".tr); + Constant.sectionConstantModel = sectionModel; + AppThemeData.primary300 = Color(int.tryParse(sectionModel.color?.replaceFirst("#", "0xff") ?? '') ?? 0xff2196F3); + if (auth.FirebaseAuth.instance.currentUser != null) { + String uid = auth.FirebaseAuth.instance.currentUser!.uid; + UserModel? user = await FireStoreUtils.getUserProfile(uid); + if (user != null && user.role == Constant.userRoleCustomer) { + user.fcmToken = await NotificationService.getToken(); + await FireStoreUtils.updateUser(user); + ShowToastDialog.closeLoader(); + await _navigate(sectionModel); + } else { + ShowToastDialog.closeLoader(); + Get.offAll(() => const LoginScreen()); + } + } else { + ShowToastDialog.closeLoader(); + await _navigate(sectionModel); + } + } catch (e) { + print("Error during service tap: $e"); + ShowToastDialog.closeLoader(); + } + } + + Future _navigate(SectionModel sectionModel) async { + await FireStoreUtils.getTaxList(sectionModel.id ?? "").then((value) { + if (value != null) { + Constant.taxList = value; + } + }); + + if (sectionModel.serviceTypeFlag == "ecommerce-service" || sectionModel.serviceTypeFlag == "delivery-service") { + if (cartItem.isNotEmpty) { + showAlertDialog(Get.context!, UserModel(), sectionModel); + } else { + if (sectionModel.serviceTypeFlag == "ecommerce-service") { + Get.to(DashBoardEcommerceScreen()); + } else if (sectionModel.serviceTypeFlag == "cab-service") { + Get.to(CabDashboardScreen()); + } else if (sectionModel.serviceTypeFlag == "rental-service") { + Get.to(RentalDashboardScreen()); + } else if (sectionModel.serviceTypeFlag == "parcel_delivery") { + Get.to(ParcelDashboardScreen()); + } else if (sectionModel.serviceTypeFlag == "ondemand-service") { + Get.to(OnDemandDashboardScreen()); + } else { + Get.to(() => DashBoardScreen()); + } + } + } else { + if (sectionModel.serviceTypeFlag == "ecommerce-service") { + Get.to(DashBoardEcommerceScreen()); + } else if (sectionModel.serviceTypeFlag == "cab-service") { + Get.to(CabDashboardScreen()); + } else if (sectionModel.serviceTypeFlag == "rental-service") { + Get.to(RentalDashboardScreen()); + } else if (sectionModel.serviceTypeFlag == "parcel_delivery") { + Get.to(ParcelDashboardScreen()); + } else if (sectionModel.serviceTypeFlag == "ondemand-service") { + Get.to(OnDemandDashboardScreen()); + } else { + Get.to(() => DashBoardScreen()); + } + } + } + + final CartProvider cartProvider = CartProvider(); + + void showAlertDialog(BuildContext context, UserModel user, SectionModel sectionModel) { + Get.defaultDialog( + title: "Alert!", + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("If you select this Section/Service, your previously added items will be removed from the cart.".tr, textAlign: TextAlign.center), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: RoundedButtonFill( + height: 5.5, + title: "Cancel".tr, + onPress: () { + Get.back(); + }, + color: AppThemeData.grey900, + textColor: AppThemeData.surface, + ), + ), + const SizedBox(width: 12), + Expanded( + child: RoundedButtonFill( + title: "OK".tr, + height: 5.5, + onPress: () async { + DatabaseHelper.instance.deleteAllCartProducts(); + cartProvider.clearDatabase(); + Get.back(); + if (sectionModel.serviceTypeFlag == "ecommerce-service") { + Get.off(() => DashBoardEcommerceScreen()); + } else { + Get.to(() => DashBoardScreen()); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.surface, + ), + ), + ], + ), + ], + ), + actions: [], // 👈 keep this empty since we put buttons in content + ); + } +} diff --git a/lib/controllers/sign_up_controller.dart b/lib/controllers/sign_up_controller.dart new file mode 100644 index 0000000..5817e97 --- /dev/null +++ b/lib/controllers/sign_up_controller.dart @@ -0,0 +1,195 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:firebase_auth/firebase_auth.dart' as auth; +import '../constant/constant.dart'; +import '../models/referral_model.dart'; +import '../screen_ui/service_home_screen/service_list_screen.dart'; +import '../service/fire_store_utils.dart'; +import '../themes/show_toast_dialog.dart'; +import '../utils/notification_service.dart'; + +class SignUpController extends GetxController { + Rx firstNameController = TextEditingController().obs; + Rx lastNameController = TextEditingController().obs; + Rx emailController = TextEditingController().obs; + Rx mobileController = TextEditingController().obs; + Rx countryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs; + Rx passwordController = TextEditingController().obs; + Rx confirmPasswordController = TextEditingController().obs; + Rx referralController = TextEditingController().obs; + + final FocusNode emailFocusNode = FocusNode(); + final FocusNode passwordFocusNode = FocusNode(); + + // State + final RxBool isLoading = false.obs; + final auth.FirebaseAuth _firebaseAuth = auth.FirebaseAuth.instance; + + RxString type = "email".obs; + Rx userModel = UserModel().obs; + + RxBool passwordVisible = true.obs; + RxBool conformPasswordVisible = true.obs; + + @override + void onInit() { + super.onInit(); + getArgument(); + } + + void getArgument() { + final args = Get.arguments; + type.value = args?['type'] ?? 'email'; + userModel.value = args?['userModel'] ?? UserModel(); + + //Pre-fill fields for Google/Apple signup + if (type.value == "google" || type.value == "apple") { + firstNameController.value.text = userModel.value.firstName ?? ""; + lastNameController.value.text = userModel.value.lastName ?? ""; + emailController.value.text = userModel.value.email ?? ""; + } + + //mobile number signup + if (type.value == "mobileNumber") { + mobileController.value.text = userModel.value.phoneNumber ?? ""; + countryCodeController.value.text = userModel.value.countryCode ?? ""; + } + } + + /// Main Sign-Up Trigger + void signUp() async { + debugPrint("SIGNUP CALLED!"); + try { + if (!_validateInputs()) return; + + ShowToastDialog.showLoader("Creating account...".tr); + + if (type.value == "mobileNumber") { + await _signUpWithMobile(); + } else { + await _signUpWithEmail(); + } + + ShowToastDialog.closeLoader(); + } catch (e, st) { + ShowToastDialog.closeLoader(); + debugPrint("SIGNUP OUTER EXCEPTION: $e\n$st"); + ShowToastDialog.showToast("${'signup_failed'.tr}: $e"); + } + } + + /// Validation Logic + bool _validateInputs() { + if (firstNameController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter first name".tr); + return false; + } else if (lastNameController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter last name".tr); + return false; + } else if (emailController.value.text.isEmpty || !emailController.value.text.isEmail) { + ShowToastDialog.showToast("Please enter a valid email address".tr); + return false; + } else if (mobileController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter a valid phone number".tr); + return false; + } else if (passwordController.value.text.length < 6) { + ShowToastDialog.showToast("Password must be at least 6 characters".tr); + return false; + } else if (passwordController.value.text != confirmPasswordController.value.text) { + ShowToastDialog.showToast("Password and Confirm password do not match".tr); + return false; + } + return true; + } + + /// Email Sign-up Flow + Future _signUpWithEmail() async { + try { + final credential = await _firebaseAuth.createUserWithEmailAndPassword(email: emailController.value.text.trim(), password: passwordController.value.text.trim()); + + if (credential.user != null) { + final newUser = await _buildUserModel(credential.user?.uid ?? ''); + await _handleReferral(newUser.id ?? ''); + await FireStoreUtils.updateUser(newUser); + // appController.currentUser.value = newUser; + _navigateBasedOnAddress(newUser); + } + } on auth.FirebaseAuthException catch (e) { + debugPrint("FirebaseAuthException caught: code=${e.code}, message=${e.message}"); + if (e.code == 'email-already-in-use') { + ShowToastDialog.showToast("Email already in use".tr); + } else if (e.code == 'weak-password') { + ShowToastDialog.showToast("Password is too weak".tr); + } else if (e.code == 'invalid-email') { + ShowToastDialog.showToast("Invalid email address".tr); + } else { + ShowToastDialog.showToast(e.message ?? "signup_failed".tr); + } + } catch (e) { + debugPrint("Something went wrong: ${e.toString()}"); + ShowToastDialog.showToast("${'something_went_wrong'.tr}: ${e.toString()}"); + } + } + + /// Mobile Sign-up Flow + Future _signUpWithMobile() async { + debugPrint("Signup with mobile called..."); + try { + final uid = FireStoreUtils.getCurrentUid(); + + userModel.value = await _buildUserModel(uid); + + await _handleReferral(uid); + await FireStoreUtils.updateUser(userModel.value); + + _navigateBasedOnAddress(userModel.value); + } catch (e) { + ShowToastDialog.showToast("${'signup_failed'.tr}: $e"); + } + } + + /// Construct UserModel + Future _buildUserModel(String uid) async { + final fcmToken = await NotificationService.getToken(); + + return UserModel( + id: uid, + firstName: firstNameController.value.text.trim(), + lastName: lastNameController.value.text.trim(), + email: emailController.value.text.trim().toLowerCase(), + phoneNumber: mobileController.value.text.trim(), + countryCode: countryCodeController.value.text.trim(), + fcmToken: fcmToken, + active: true, + createdAt: Timestamp.now(), + role: Constant.userRoleCustomer, + ); + } + + /// Handle Referral Logic + Future _handleReferral(String userId) async { + final referralCode = referralController.value.text.trim(); + final referralBy = referralCode.isNotEmpty ? (await FireStoreUtils.getReferralUserByCode(referralCode))?.id ?? '' : ''; + + final referral = ReferralModel(id: userId, referralBy: referralBy, referralCode: Constant.getReferralCode()); + + await FireStoreUtils.referralAdd(referral); + } + + /// Navigate Based on Shipping Address + void _navigateBasedOnAddress(UserModel user) { + if (user.shippingAddress?.isNotEmpty == true) { + final defaultAddress = user.shippingAddress!.firstWhere((e) => e.isDefault == true, orElse: () => user.shippingAddress!.first); + + /// Save the default address to global constant + Constant.selectedLocation = defaultAddress; + + Get.offAll(() => const ServiceListScreen()); + } else { + Get.offAll(() => const LocationPermissionScreen()); + } + } +} diff --git a/lib/controllers/splash_controller.dart b/lib/controllers/splash_controller.dart new file mode 100644 index 0000000..3042224 --- /dev/null +++ b/lib/controllers/splash_controller.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/maintenance_mode_screen/maintenance_mode_screen.dart'; +import 'package:customer/screen_ui/service_home_screen/service_list_screen.dart'; +import 'package:customer/utils/notification_service.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:get/get.dart'; +import '../screen_ui/auth_screens/login_screen.dart'; +import '../screen_ui/location_enable_screens/location_permission_screen.dart'; +import '../screen_ui/on_boarding_screen/on_boarding_screen.dart'; +import '../service/fire_store_utils.dart'; + +class SplashController extends GetxController { + @override + void onInit() { + Timer(const Duration(seconds: 3), () => redirectScreen()); + super.onInit(); + } + + Future redirectScreen() async { + if (Constant.isMaintenanceModeForCustomer == true) { + Get.offAll(const MaintenanceModeScreen()); + return; + } + if (Preferences.getBoolean(Preferences.isFinishOnBoardingKey) == false) { + Get.offAll(const OnboardingScreen()); + } else { + bool isLogin = await FireStoreUtils.isLogin(); + if (isLogin == true) { + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) async { + if (value != null) { + UserModel userModel = value; + log(userModel.toJson().toString()); + if (userModel.role == Constant.userRoleCustomer) { + if (userModel.active == true) { + userModel.fcmToken = await NotificationService.getToken(); + await FireStoreUtils.updateUser(userModel); + if (userModel.shippingAddress != null && userModel.shippingAddress!.isNotEmpty) { + if (userModel.shippingAddress!.where((element) => element.isDefault == true).isNotEmpty) { + Constant.selectedLocation = userModel.shippingAddress!.where((element) => element.isDefault == true).single; + } else { + Constant.selectedLocation = userModel.shippingAddress!.first; + } + Get.offAll(const ServiceListScreen()); + } else { + Get.offAll(const LocationPermissionScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + Get.offAll(const LoginScreen()); + } + } else { + await FirebaseAuth.instance.signOut(); + Get.offAll(const LoginScreen()); + } + } + }); + } else { + await FirebaseAuth.instance.signOut(); + Get.offAll(const LoginScreen()); + } + } + } +} diff --git a/lib/controllers/theme_controller.dart b/lib/controllers/theme_controller.dart new file mode 100644 index 0000000..64b05d6 --- /dev/null +++ b/lib/controllers/theme_controller.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../utils/preferences.dart'; + +class ThemeController extends GetxController { + RxBool isDark = false.obs; + + @override + void onInit() { + super.onInit(); + loadTheme(); + } + + void loadTheme() { + // Use safe getBoolean from Preferences + isDark.value = Preferences.getBoolean(Preferences.themKey); + } + + void toggleTheme() { + isDark.value = !isDark.value; + Preferences.setBoolean(Preferences.themKey, isDark.value); + } + + ThemeMode get themeMode => isDark.value ? ThemeMode.dark : ThemeMode.light; +} + + diff --git a/lib/controllers/view_all_category_controller.dart b/lib/controllers/view_all_category_controller.dart new file mode 100644 index 0000000..1612e88 --- /dev/null +++ b/lib/controllers/view_all_category_controller.dart @@ -0,0 +1,31 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import '../service/fire_store_utils.dart'; +import 'package:get/get.dart'; + +class ViewAllCategoryController extends GetxController { + RxBool isLoading = true.obs; + + RxList vendorCategoryModel = [].obs; + + @override + void onInit() { + // TODO: implement onInit + getCategoryData(); + super.onInit(); + } + + Future getCategoryData() async { + await FireStoreUtils.getVendorCategory().then((value) { + vendorCategoryModel.value = value; + + }); + + if (Constant.restaurantList != null) { + List usedCategoryIds = Constant.restaurantList!.expand((vendor) => vendor.categoryID ?? []).whereType().toSet().toList(); + vendorCategoryModel.value = vendorCategoryModel.where((category) => usedCategoryIds.contains(category.id)).toList(); + } + + isLoading.value = false; + } +} diff --git a/lib/controllers/view_all_popular_service_controller.dart b/lib/controllers/view_all_popular_service_controller.dart new file mode 100644 index 0000000..24da089 --- /dev/null +++ b/lib/controllers/view_all_popular_service_controller.dart @@ -0,0 +1,83 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/on_demand_home_controller.dart'; +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../models/favorite_ondemand_service_model.dart'; +import '../models/provider_serivce_model.dart'; +import '../service/fire_store_utils.dart'; + +class ViewAllPopularServiceController extends GetxController { + RxList providerList = [].obs; + RxList allProviderList = [].obs; + RxBool isLoading = true.obs; + Rx onDemandHomeController = Get.find().obs; + + final OnDemandHomeController onDemandController = Get.find(); + + Rx searchTextFiledController = TextEditingController().obs; + + RxList lstFav = [].obs; + + @override + void onInit() { + super.onInit(); + getData(); + } + + Future getData() async { + isLoading.value = true; + + await FireStoreUtils.getProviderFuture() + .then((providerServiceList) { + Set uniqueAuthorIds = providerServiceList.map((service) => service.author).toSet(); + List listOfUniqueProviders = uniqueAuthorIds.toList(); + + List filteredProviders = []; + + for (var provider in listOfUniqueProviders) { + List filteredList = providerServiceList.where((service) => service.author == provider).toList(); + + filteredList.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); + + for (int index = 0; index < filteredList.length; index++) { + final service = filteredList[index]; + + if (Constant.isSubscriptionModelApplied == true || Constant.sectionConstantModel?.adminCommision?.isEnabled == true) { + if (service.subscriptionPlan?.itemLimit == "-1") { + filteredProviders.add(service); + } else { + if (index < int.parse(service.subscriptionPlan?.itemLimit ?? '0')) { + filteredProviders.add(service); + } + } + } else { + filteredProviders.add(service); + } + } + } + + allProviderList.value = filteredProviders; + providerList.value = filteredProviders; + isLoading.value = false; + }) + .catchError((e) { + print("Provider error: $e"); + isLoading.value = false; + }); + + if (Constant.userModel != null) { + await FireStoreUtils.getFavouritesServiceList(FireStoreUtils.getCurrentUid()).then((value) { + lstFav.value = value; + }); + } + isLoading.value = false; + } + + void getFilterData(String value) { + if (value.isNotEmpty) { + providerList.value = allProviderList.where((e) => e.title!.toLowerCase().contains(value.toLowerCase()) || e.title!.startsWith(value)).toList(); + } else { + providerList.assignAll(allProviderList); + } + } +} diff --git a/lib/controllers/view_category_service_controller.dart b/lib/controllers/view_category_service_controller.dart new file mode 100644 index 0000000..58cdaf6 --- /dev/null +++ b/lib/controllers/view_category_service_controller.dart @@ -0,0 +1,65 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/on_demand_home_controller.dart'; +import 'package:get/get.dart'; +import '../models/provider_serivce_model.dart'; +import '../service/fire_store_utils.dart'; + +class ViewCategoryServiceController extends GetxController { + RxBool isLoading = true.obs; + RxList providerList = [].obs; + + RxString categoryId = "".obs, categoryTitle = "".obs; + Rx onDemandHomeController = Get.find().obs; + + @override + void onInit() { + super.onInit(); + + final args = Get.arguments as Map; + categoryId.value = args['categoryId'] ?? ""; + categoryTitle.value = args['categoryTitle'] ?? ""; + + getData(); + } + + Future getData() async { + providerList.clear(); + isLoading.value = true; + + List providerServiceList = await FireStoreUtils.getProviderFuture(categoryId: categoryId.value); + + List uniqueAuthId = providerServiceList.map((service) => service.author).toList(); + List uniqueServiceId = providerServiceList.map((service) => service.id).toList(); + + List filterByItemLimit = []; + List uniqueId = []; + + if ((Constant.isSubscriptionModelApplied == true || Constant.sectionConstantModel!.adminCommision?.isEnabled == true)) { + for (var authUser in uniqueAuthId) { + List listofAllServiceByAuth = await FireStoreUtils.getAllProviderServiceByAuthorId(authUser!); + + for (int i = 0; i < listofAllServiceByAuth.length; i++) { + if (listofAllServiceByAuth[i].subscriptionPlan?.itemLimit != null && + (i < int.parse(listofAllServiceByAuth[i].subscriptionPlan?.itemLimit ?? '0') || listofAllServiceByAuth[i].subscriptionPlan?.itemLimit == '-1')) { + if (uniqueServiceId.contains(listofAllServiceByAuth[i].id)) { + filterByItemLimit.add(listofAllServiceByAuth[i]); + } + } + } + + for (var service in filterByItemLimit) { + for (var unique in uniqueServiceId) { + if (service.id == unique && !uniqueId.contains(service.id) && service.subscriptionTotalOrders != '0') { + uniqueId.add(service.id); + providerList.add(service); + } + } + } + } + } else { + providerList.addAll(providerServiceList); + } + + isLoading.value = false; + } +} diff --git a/lib/controllers/wallet_controller.dart b/lib/controllers/wallet_controller.dart new file mode 100644 index 0000000..a955701 --- /dev/null +++ b/lib/controllers/wallet_controller.dart @@ -0,0 +1,601 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as maths; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/payment_model/flutter_wave_model.dart'; +import 'package:customer/models/payment_model/mercado_pago_model.dart'; +import 'package:customer/models/payment_model/mid_trans.dart'; +import 'package:customer/models/payment_model/orange_money.dart'; +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:customer/models/payment_model/pay_stack_model.dart'; +import 'package:customer/models/payment_model/paypal_model.dart'; +import 'package:customer/models/payment_model/razorpay_model.dart'; +import 'package:customer/models/payment_model/stripe_model.dart'; +import 'package:customer/models/payment_model/xendit.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter_paypal/flutter_paypal.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import '../payment/MercadoPagoScreen.dart'; +import '../payment/PayFastScreen.dart'; +import '../payment/midtrans_screen.dart'; +import '../payment/orangePayScreen.dart'; +import '../payment/paystack/pay_stack_screen.dart'; +import '../payment/paystack/pay_stack_url_model.dart'; +import '../payment/paystack/paystack_url_genrater.dart'; +import '../payment/stripe_failed_model.dart'; +import '../payment/xenditModel.dart'; +import '../payment/xenditScreen.dart'; +import '../service/fire_store_utils.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; + +import '../themes/show_toast_dialog.dart'; + +class WalletController extends GetxController { + RxBool isLoading = true.obs; + + Rx topUpAmountController = TextEditingController().obs; + + RxList walletTransactionList = [].obs; + + Rx userModel = UserModel().obs; + RxString selectedPaymentMethod = "".obs; + + @override + void onInit() { + // TODO: implement onInit + getPaymentSettings(); + getWalletTransaction(); + super.onInit(); + } + + Rx payFastModel = PayFastModel().obs; + Rx mercadoPagoModel = MercadoPagoModel().obs; + Rx payPalModel = PayPalModel().obs; + Rx stripeModel = StripeModel().obs; + Rx flutterWaveModel = FlutterWaveModel().obs; + Rx payStackModel = PayStackModel().obs; + Rx razorPayModel = RazorPayModel().obs; + Rx midTransModel = MidTrans().obs; + Rx orangeMoneyModel = OrangeMoney().obs; + Rx xenditModel = Xendit().obs; + + Future getPaymentSettings() async { + await FireStoreUtils.getPaymentSettingsData().then((value) { + payFastModel.value = PayFastModel.fromJson(jsonDecode(Preferences.getString(Preferences.payFastSettings))); + mercadoPagoModel.value = MercadoPagoModel.fromJson(jsonDecode(Preferences.getString(Preferences.mercadoPago))); + payPalModel.value = PayPalModel.fromJson(jsonDecode(Preferences.getString(Preferences.paypalSettings))); + stripeModel.value = StripeModel.fromJson(jsonDecode(Preferences.getString(Preferences.stripeSettings))); + flutterWaveModel.value = FlutterWaveModel.fromJson(jsonDecode(Preferences.getString(Preferences.flutterWave))); + payStackModel.value = PayStackModel.fromJson(jsonDecode(Preferences.getString(Preferences.payStack))); + razorPayModel.value = RazorPayModel.fromJson(jsonDecode(Preferences.getString(Preferences.razorpaySettings))); + + midTransModel.value = MidTrans.fromJson(jsonDecode(Preferences.getString(Preferences.midTransSettings))); + orangeMoneyModel.value = OrangeMoney.fromJson(json.decode(Preferences.getString(Preferences.orangeMoneySettings))); + xenditModel.value = Xendit.fromJson(jsonDecode(Preferences.getString(Preferences.xenditSettings))); + + Stripe.publishableKey = stripeModel.value.clientpublishableKey.toString(); + Stripe.merchantIdentifier = 'GoRide'; + Stripe.instance.applySettings(); + setRef(); + + razorPay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlePaymentSuccess); + razorPay.on(Razorpay.EVENT_EXTERNAL_WALLET, handleExternalWaller); + razorPay.on(Razorpay.EVENT_PAYMENT_ERROR, handlePaymentError); + }); + } + + Future getWalletTransaction() async { + if (Constant.userModel != null) { + await FireStoreUtils.getWalletTransaction().then((value) { + if (value != null) { + walletTransactionList.value = value; + } + }); + await FireStoreUtils.getUserProfile(FireStoreUtils.getCurrentUid()).then((value) { + if (value != null) { + userModel.value = value; + } + }); + } + isLoading.value = false; + } + + Future walletTopUp() async { + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(topUpAmountController.value.text), + date: Timestamp.now(), + paymentMethod: selectedPaymentMethod.value, + transactionUser: "user", + userId: FireStoreUtils.getCurrentUid(), + isTopup: true, + note: "Wallet Top-up", + paymentStatus: "success", + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: topUpAmountController.value.text, userId: FireStoreUtils.getCurrentUid()).then((value) { + getWalletTransaction(); + Get.back(); + }); + } + }); + + ShowToastDialog.showToast("Amount Top-up successfully".tr); + } + + // Strip + Future stripeMakePayment({required String amount}) async { + log(double.parse(amount).toStringAsFixed(0)); + try { + Map? paymentIntentData = await createStripeIntent(amount: amount); + log("stripe Responce====>$paymentIntentData"); + if (paymentIntentData!.containsKey("error")) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: paymentIntentData['client_secret'], + allowsDelayedPaymentMethods: false, + googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true, currencyCode: "USD"), + customFlow: true, + style: ThemeMode.system, + appearance: PaymentSheetAppearance(colors: PaymentSheetAppearanceColors(primary: AppThemeData.primary300)), + merchantDisplayName: 'GoRide', + ), + ); + displayStripePaymentSheet(amount: amount); + } + } catch (e, s) { + log("$e \n$s"); + ShowToastDialog.showToast("exception:$e \n$s"); + } + } + + Future displayStripePaymentSheet({required String amount}) async { + try { + await Stripe.instance.presentPaymentSheet().then((value) { + ShowToastDialog.showToast("Payment successfully".tr); + walletTopUp(); + }); + } on StripeException catch (e) { + var lo1 = jsonEncode(e); + var lo2 = jsonDecode(lo1); + StripePayFailedModel lom = StripePayFailedModel.fromJson(lo2); + ShowToastDialog.showToast(lom.error.message); + } catch (e) { + ShowToastDialog.showToast(e.toString()); + } + } + + Future createStripeIntent({required String amount}) async { + try { + Map body = { + 'amount': ((double.parse(amount) * 100).round()).toString(), + 'currency': "USD", + 'payment_method_types[]': 'card', + "description": "Strip Payment", + "shipping[name]": userModel.value.fullName(), + "shipping[address][line1]": "510 Townsend St", + "shipping[address][postal_code]": "98140", + "shipping[address][city]": "San Francisco", + "shipping[address][state]": "CA", + "shipping[address][country]": "US", + }; + var stripeSecret = stripeModel.value.stripeSecret; + var response = await http.post( + Uri.parse('https://api.stripe.com/v1/payment_intents'), + body: body, + headers: {'Authorization': 'Bearer $stripeSecret', 'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + return jsonDecode(response.body); + } catch (e) { + log(e.toString()); + } + } + + //mercadoo + Future mercadoPagoMakePayment({required BuildContext context, required String amount}) async { + final headers = {'Authorization': 'Bearer ${mercadoPagoModel.value.accessToken}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "items": [ + { + "title": "Test", + "description": "Test Payment", + "quantity": 1, + "currency_id": "BRL", // or your preferred currency + "unit_price": double.parse(amount), + }, + ], + "payer": {"email": userModel.value.email}, + "back_urls": {"failure": "${Constant.globalUrl}payment/failure", "pending": "${Constant.globalUrl}payment/pending", "success": "${Constant.globalUrl}payment/success"}, + "auto_return": "approved", // Automatically return after payment is approved + }); + + final response = await http.post(Uri.parse("https://api.mercadopago.com/checkout/preferences"), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['init_point']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something want wrong please contact administrator".tr); + print('Error creating preference: ${response.body}'); + return null; + } + } + + void paypalPaymentSheet(String amount, context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: + (BuildContext context) => UsePaypal( + sandboxMode: payPalModel.value.isLive == true ? false : true, + clientId: payPalModel.value.paypalClient ?? '', + secretKey: payPalModel.value.paypalSecret ?? '', + returnURL: "com.parkme://paypalpay", + cancelURL: "com.parkme://paypalpay", + transactions: [ + { + "amount": { + "total": amount, + "currency": "USD", + "details": {"subtotal": amount}, + }, + }, + ], + note: "Contact us for any questions on your order.", + onSuccess: (Map params) async { + walletTopUp(); + ShowToastDialog.showToast("Payment Successful!!".tr); + }, + onError: (error) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + onCancel: (params) { + Get.back(); + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + }, + ), + ), + ); + } + + ///PayStack Payment Method + Future payStackPayment(String totalAmount) async { + await PayStackURLGen.payStackURLGen(amount: (double.parse(totalAmount) * 100).toString(), currency: "ZAR", secretKey: payStackModel.value.secretKey.toString(), userModel: userModel.value).then(( + value, + ) async { + if (value != null) { + PayStackUrlModel payStackModel0 = value; + Get.to( + PayStackScreen( + secretKey: payStackModel.value.secretKey.toString(), + callBackUrl: payStackModel.value.callbackURL.toString(), + initialURl: payStackModel0.data.authorizationUrl, + amount: totalAmount, + reference: payStackModel0.data.reference, + ), + )!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } + }); + } + + //flutter wave Payment Method + Future flutterWaveInitiatePayment({required BuildContext context, required String amount}) async { + final url = Uri.parse('https://api.flutterwave.com/v3/payments'); + final headers = {'Authorization': 'Bearer ${flutterWaveModel.value.secretKey}', 'Content-Type': 'application/json'}; + + final body = jsonEncode({ + "tx_ref": _ref, + "amount": amount, + "currency": "NGN", + "redirect_url": "${Constant.globalUrl}payment/success", + "payment_options": "ussd, card, barter, payattitude", + "customer": { + "email": userModel.value.email.toString(), + "phonenumber": userModel.value.phoneNumber, // Add a real phone number + "name": userModel.value.fullName(), // Add a real customer name + }, + "customizations": {"title": "Payment for Services", "description": "Payment for XYZ services"}, + }); + + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + Get.to(MercadoPagoScreen(initialURl: data['data']['link']))!.then((value) { + if (value) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } else { + ShowToastDialog.showToast("Payment UnSuccessful!!".tr); + } + }); + } else { + print('Payment initialization failed: ${response.body}'); + return null; + } + } + + String? _ref; + + void setRef() { + maths.Random numRef = maths.Random(); + int year = DateTime.now().year; + int refNumber = numRef.nextInt(20000); + if (Platform.isAndroid) { + _ref = "AndroidRef$year$refNumber"; + } else if (Platform.isIOS) { + _ref = "IOSRef$year$refNumber"; + } + } + + // payFast + void payFastPayment({required BuildContext context, required String amount}) { + PayStackURLGen.getPayHTML(payFastSettingData: payFastModel.value, amount: amount.toString(), userModel: userModel.value).then((String? value) async { + bool isDone = await Get.to(PayFastScreen(htmlData: value!, payFastSettingData: payFastModel.value)); + if (isDone) { + Get.back(); + ShowToastDialog.showToast("Payment successfully".tr); + walletTopUp(); + } else { + Get.back(); + ShowToastDialog.showToast("Payment Failed".tr); + } + }); + } + + ///RazorPay payment function + final Razorpay razorPay = Razorpay(); + + void openCheckout({required amount, required orderId}) async { + var options = { + 'key': razorPayModel.value.razorpayKey, + 'amount': amount * 100, + 'name': 'GoRide', + 'order_id': orderId, + "currency": "INR", + 'description': 'wallet Topup', + 'retry': {'enabled': true, 'max_count': 1}, + 'send_sms_hash': true, + 'prefill': {'contact': userModel.value.phoneNumber, 'email': userModel.value.email}, + 'external': { + 'wallets': ['paytm'], + }, + }; + + try { + razorPay.open(options); + } catch (e) { + debugPrint('Error: $e'); + } + } + + void handlePaymentSuccess(PaymentSuccessResponse response) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } + + void handleExternalWaller(ExternalWalletResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Processing!! via".tr); + } + + void handlePaymentError(PaymentFailureResponse response) { + Get.back(); + ShowToastDialog.showToast("Payment Failed!!".tr); + } + + //Midtrans payment + Future midtransMakePayment({required String amount, required BuildContext context}) async { + await createPaymentLink(amount: amount).then((url) { + ShowToastDialog.closeLoader(); + if (url != '') { + Get.to(() => MidtransScreen(initialURl: url))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createPaymentLink({required var amount}) async { + var ordersId = const Uuid().v1(); + final url = Uri.parse(midTransModel.value.isSandbox! ? 'https://api.sandbox.midtrans.com/v1/payment-links' : 'https://api.midtrans.com/v1/payment-links'); + + final response = await http.post( + url, + headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(midTransModel.value.serverKey!)}, + body: jsonEncode({ + 'transaction_details': {'order_id': ordersId, 'gross_amount': double.parse(amount.toString()).toInt()}, + 'usage_limit': 2, + "callbacks": {"finish": "https://www.google.com?merchant_order_id=$ordersId"}, + }), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = jsonDecode(response.body); + return responseData['payment_url']; + } else { + ShowToastDialog.showToast("something went wrong, please contact admin.".tr); + return ''; + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } + + //Orangepay payment + static String accessToken = ''; + static String payToken = ''; + static String orderId = ''; + static String amount = ''; + + Future orangeMakePayment({required String amount, required BuildContext context}) async { + reset(); + var id = const Uuid().v4(); + var paymentURL = await fetchToken(context: context, orderId: id, amount: amount, currency: 'USD'); + ShowToastDialog.closeLoader(); + if (paymentURL.toString() != '') { + Get.to(() => OrangeMoneyScreen(initialURl: paymentURL, accessToken: accessToken, amount: amount, orangePay: orangeMoneyModel.value, orderId: orderId, payToken: payToken))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } + }); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + } + + Future fetchToken({required String orderId, required String currency, required BuildContext context, required String amount}) async { + String apiUrl = 'https://api.orange.com/oauth/v3/token'; + Map requestBody = {'grant_type': 'client_credentials'}; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': "Basic ${orangeMoneyModel.value.auth!}", 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, + body: requestBody, + ); + + if (response.statusCode == 200) { + Map responseData = jsonDecode(response.body); + + accessToken = responseData['access_token']; + return await webpayment(context: context, amountData: amount, currency: currency, orderIdData: orderId); + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + Future webpayment({required String orderIdData, required BuildContext context, required String currency, required String amountData}) async { + orderId = orderIdData; + amount = amountData; + String apiUrl = orangeMoneyModel.value.isSandbox! == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/webpayment' : 'https://api.orange.com/orange-money-webpay/cm/v1/webpayment'; + Map requestBody = { + "merchant_key": orangeMoneyModel.value.merchantKey ?? '', + "currency": orangeMoneyModel.value.isSandbox == true ? "OUV" : currency, + "order_id": orderId, + "amount": amount, + "reference": 'Y-Note Test', + "lang": "en", + "return_url": orangeMoneyModel.value.returnUrl!.toString(), + "cancel_url": orangeMoneyModel.value.cancelUrl!.toString(), + "notif_url": orangeMoneyModel.value.notifUrl!.toString(), + }; + + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + print(response.statusCode); + print(response.body); + + // Handle the response + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + if (responseData['message'] == 'OK') { + payToken = responseData['pay_token']; + return responseData['payment_url']; + } else { + return ''; + } + } else { + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + return ''; + } + } + + static void reset() { + accessToken = ''; + payToken = ''; + orderId = ''; + amount = ''; + } + + //XenditPayment + Future xenditPayment(context, amount) async { + await createXenditInvoice(amount: amount).then((model) { + ShowToastDialog.closeLoader(); + if (model.id != null) { + Get.to(() => XenditScreen(initialURl: model.invoiceUrl ?? '', transId: model.id ?? '', apiKey: xenditModel.value.apiKey!.toString()))!.then((value) { + if (value == true) { + ShowToastDialog.showToast("Payment Successful!!".tr); + walletTopUp(); + } else { + ShowToastDialog.showToast("Payment Unsuccessful!!".tr); + } + }); + } + }); + } + + Future createXenditInvoice({required var amount}) async { + const url = 'https://api.xendit.co/v2/invoices'; + var headers = { + 'Content-Type': 'application/json', + 'Authorization': generateBasicAuthHeader(xenditModel.value.apiKey!.toString()), + // 'Cookie': '__cf_bm=yERkrx3xDITyFGiou0bbKY1bi7xEwovHNwxV1vCNbVc-1724155511-1.0.1.1-jekyYQmPCwY6vIJ524K0V6_CEw6O.dAwOmQnHtwmaXO_MfTrdnmZMka0KZvjukQgXu5B.K_6FJm47SGOPeWviQ', + }; + + final body = jsonEncode({ + 'external_id': const Uuid().v1(), + 'amount': amount, + 'payer_email': 'customer@domain.com', + 'description': 'Test - VA Successful invoice payment', + 'currency': 'IDR', //IDR, PHP, THB, VND, MYR + }); + + try { + final response = await http.post(Uri.parse(url), headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } catch (e) { + return XenditModel(); + } + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..f77a606 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,71 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyALZhdy7Rw3jffipxsDvvz7_C_b4teVg1k', + appId: '1:893074789710:android:05002c15a64cf1e0c4ba1f', + messagingSenderId: '893074789710', + projectId: 'fondexuzb', + databaseURL: 'https://fondexuzb-default-rtdb.firebaseio.com', + storageBucket: 'fondexuzb.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyD6Khoz4y93GCj3mOPi2FoluDipplH1av0', + appId: '1:893074789710:ios:510ebf9e4ebed6a8c4ba1f', + messagingSenderId: '893074789710', + projectId: 'fondexuzb', + databaseURL: 'https://fondexuzb-default-rtdb.firebaseio.com', + storageBucket: 'fondexuzb.firebasestorage.app', + iosClientId: '893074789710-pv12m4nhe82a4ueg9sb2pgt42r0e5da3.apps.googleusercontent.com', + iosBundleId: 'com.emart.customer', + ); + +} \ No newline at end of file diff --git a/lib/lang/app_en.dart b/lib/lang/app_en.dart new file mode 100644 index 0000000..e451815 --- /dev/null +++ b/lib/lang/app_en.dart @@ -0,0 +1,784 @@ +const Map enUS = { + 'Let’s Get Started': 'Let’s Get Started', + 'Skip': 'Skip', + 'Next': 'Next', + 'Log in to explore your all in one vendor app favourites and shop effortlessly.': 'Log in to explore your all in one vendor app favourites and shop effortlessly.', + 'Email Address*': 'Email Address*', + 'jerome014@gmail.com': 'jerome014@gmail.com', + 'Password*': 'Password*', + 'Enter password': 'Enter password', + 'Forgot Password': 'Forgot Password', + 'Log in': 'Log in', + 'or continue with': 'or continue with', + 'Mobile number': 'Mobile number', + 'with Google': 'with Google', + 'with Apple': 'with Apple', + "Didn't have an account?": "Didn't have an account?", + 'Sign up': 'Sign up', + 'Please enter a valid email address': 'Please enter a valid email address', + 'Please enter your password': 'Please enter your password', + 'Logging in...': 'Logging in...', + 'This user is disabled. Please contact admin.': 'This user is disabled. Please contact admin.', + 'This user does not exist in the customer app.': 'This user does not exist in the customer app.', + 'No user found for that email.': 'No user found for that email.', + 'Wrong password provided.': 'Wrong password provided.', + 'Invalid email.': 'Invalid email.', + 'Login failed. Please try again.': 'Login failed. Please try again.', + 'please wait...': 'please wait...', + 'Sign up to explore all our services and start shopping, riding, and more.': 'Sign up to explore all our services and start shopping, riding, and more.', + 'First Name*': 'First Name*', + 'Jerome': 'Jerome', + 'KM': 'KM', + 'Last Name*': 'Last Name*', + 'Bell': 'Bell', + 'Mobile Number*': 'Mobile Number*', + 'Enter Mobile number': 'Enter Mobile number', + 'Confirm Password*': 'Confirm Password*', + 'Enter confirm password': 'Enter confirm password', + 'Referral Code': 'Referral Code', + 'Enter referral code': 'Enter referral code', + 'Already have an account?': 'Already have an account?', + 'Creating account...': 'Creating account...', + 'signup_failed': 'Signup failed', + 'Please enter first name': 'Please enter first name', + 'Please enter last name': 'Please enter last name', + 'Please enter a valid phone number': 'Please enter a valid phone number', + 'Password must be at least 6 characters': 'Password must be at least 6 characters', + 'Password and Confirm password do not match': 'Password and Confirm password do not match', + 'Email already in use': 'Email already in use', + 'Password is too weak': 'Password is too weak', + 'Invalid email address': 'Invalid email address', + 'something_went_wrong': 'Something went wrong', + 'Enter your registered email to receive a reset link.': 'Enter your registered email to receive a reset link.', + 'Send Link': 'Send Link', + 'Remember Password?': 'Remember Password?', + 'Please enter your email address.': 'Please enter your email address.', + 'Please enter a valid email address.': 'Please enter a valid email address.', + "reset_password_link_sent": "Reset password link sent to @email", + 'Use your mobile number to Log in easily and securely.': 'Use your mobile number to Log in easily and securely.', + 'Send Code': 'Send Code', + 'Email address': 'Email address', + 'Please enter a valid 10-digit mobile number': 'Please enter a valid 10-digit mobile number', + 'Sending OTP...': 'Sending OTP...', + 'Invalid phone number': 'Invalid phone number', + 'OTP verification failed': 'OTP verification failed', + 'Something went wrong. Please try again.': 'Something went wrong. Please try again.', + 'Enter the OTP sent to your mobile': 'Enter the OTP sent to your mobile', + 'Resend OTP': 'Resend OTP', + 'Verify': 'Verify', + 'OTP sent': 'OTP sent', + 'Enter valid 6-digit OTP': 'Enter valid 6-digit OTP', + 'Verifying OTP...': 'Verifying OTP...', + 'This user is disabled': 'This user is disabled', + 'Invalid OTP or Verification Failed': 'Invalid OTP or Verification Failed', + 'eMart': 'eMart', + 'All Your Needs in One App!': 'All Your Needs in One App!', + 'Explore Our Services': 'Explore Our Services', + 'Alert!': 'Alert!', + 'Alert': 'Alert', + 'If you select this Section/Service, your previously added items will be removed from the cart.': 'If you select this Section/Service, your previously added items will be removed from the cart.', + 'Cancel': 'Cancel', + 'OK': 'OK', + 'Home': 'Home', + 'My Bookings': 'My Bookings', + 'Favourites': 'Favourites', + 'Orders': 'Orders', + 'Profile': 'Profile', + 'Wallet': 'Wallet', + 'Service is unavailable at the selected address.': 'Service is unavailable at the selected address.', + 'Pickup Location': 'Pickup Location', + 'Destination Location': 'Destination Location', + 'Continue': 'Continue', + 'Please select source location': 'Please select source location', + 'Please select destination location': 'Please select destination location', + 'Select Your Vehicle Type': 'Select Your Vehicle Type', + "pay_amount": "Pay @amount", + 'Please select a vehicle type first.': 'Please select a vehicle type first.', + 'Select Payment Method': 'Select Payment Method', + 'Preferred Payment': 'Preferred Payment', + 'Other Payment Options': 'Other Payment Options', + 'Please select a payment method': 'Please select a payment method', + 'Insufficient wallet balance. Please select another payment method.': 'Insufficient wallet balance. Please select another payment method.', + 'Promo code': 'Promo code', + 'Promo Code': 'Promo Code', + 'Apply promo code': 'Apply promo code', + 'This offer not eligible for this booking': 'This offer not eligible for this booking', + 'View All': 'View All', + 'Write coupon Code': 'Write coupon Code', + 'Redeem now': 'Redeem now', + 'Please enter a coupon code': 'Please enter a coupon code', + 'Coupon applied successfully': 'Coupon applied successfully', + 'This coupon code has been expired': 'This coupon code has been expired', + 'Invalid coupon code': 'Invalid coupon code', + 'Order Summary': 'Order Summary', + 'Subtotal': 'Subtotal', + 'SubTotal': 'SubTotal', + 'Discount': 'Discount', + 'Order Total': 'Order Total', + 'Confirm Booking': 'Confirm Booking', + 'Waiting for driver....': 'Waiting for driver....', + 'Cancel Ride': 'Cancel Ride', + 'Ride cancelled successfully': 'Ride cancelled successfully', + 'Failed to cancel ride': 'Failed to cancel ride', + 'Otp :': 'Otp :', + 'SOS': 'SOS', + 'Please wait...': 'Please wait...', + 'Your SOS request has been submitted to admin': 'Your SOS request has been submitted to admin', + 'Your SOS request is already submitted': 'Your SOS request is already submitted', + 'Pay Now': 'Pay Now', + 'Something went wrong, please contact admin.': 'Something went wrong, please contact admin.', + 'Please select payment method': 'Please select payment method', + 'Driver at Pickup': 'Driver at Pickup', + 'Driver Location': 'Driver Location', + 'Payment method changed': 'Payment method changed', + 'Payment successfully': 'Payment successfully', + 'Payment Successful!!': 'Payment Successful!!', + 'Payment UnSuccessful!!': 'Payment UnSuccessful!!', + 'Payment Failed': 'Payment Failed', + 'Payment Processing!! via': 'Payment Processing!! via', + 'Payment Failed!!': 'Payment Failed!!', + 'Coupon': 'Coupon', + 'Coupon not found': 'Coupon not found', + 'Tap To Apply': 'Tap To Apply', + 'My Booking': 'My Booking', + 'Login': 'Login', + 'Where are you going for?': 'Where are you going for?', + 'Ride': 'Ride', + 'City rides, 24x7 availability': 'City rides, 24x7 availability', + 'Intercity/Outstation': 'Intercity/Outstation', + 'Long trips, prepaid options': 'Long trips, prepaid options', + 'Every Ride. Every Driver. Verified.': 'Every Ride. Every Driver. Verified.', + 'All drivers go through ID checks and background verification for your safety.': 'All drivers go through ID checks and background verification for your safety.', + 'Ride Details': 'Ride Details', + 'Order Id:': 'Order Id:', + 'Booking Date:': 'Booking Date:', + 'Ride & Fare Summary': 'Ride & Fare Summary', + 'Add Review': 'Add Review', + 'Complain': 'Complain', + 'Distance': 'Distance', + 'Duration': 'Duration', + 'Update Review': 'Update Review', + 'How is your trip?': 'How is your trip?', + 'Your feedback will help us improve \n driving experience better': 'Your feedback will help us improve \n driving experience better', + 'Rate for': 'Rate for', + 'Type comment....': 'Type comment....', + 'Please provide rating and comment': 'Please provide rating and comment', + 'Submit in...': 'Submit in...', + 'Title': 'Title', + 'Save': 'Save', + 'Type Description....': 'Type Description....', + 'Order data not found': 'Order data not found', + 'Failed to load complaint': 'Failed to load complaint', + 'Please enter complaint title': 'Please enter complaint title', + 'Please enter complaint description': 'Please enter complaint description', + 'Your complaint has been submitted to admin': 'Your complaint has been submitted to admin', + 'Your complaint is already submitted': 'Your complaint is already submitted', + 'Something went wrong, please try again': 'Something went wrong, please try again', + 'Popular Destinations': 'Popular Destinations', + 'Ride History': 'Ride History', + 'No order found': 'No order found', + 'You do not have sufficient wallet balance': 'You do not have sufficient wallet balance', + "new_tab": "New", + "on_going_tab": "On Going", + "completed_tab": "Completed", + "cancelled_tab": "Cancelled", + 'New': 'New', + 'On Going': 'On Going', + 'Completed': 'Completed', + 'Cancelled': 'Cancelled', + 'My Addresses': 'My Addresses', + 'Allows users to view, manage, add, or edit delivery addresses': 'Allows users to view, manage, add, or edit delivery addresses', + 'Address not found': 'Address not found', + 'Default': 'Default', + 'Add New Address': 'Add New Address', + 'Edit Address': 'Edit Address', + 'Add a New Address': 'Add a New Address', + 'Enter your location details so we can deliver your orders quickly and accurately.': 'Enter your location details so we can deliver your orders quickly and accurately.', + 'Set as Default Address': 'Set as Default Address', + 'Choose Location': 'Choose Location', + 'Flat/House/Floor/Building*': 'Flat/House/Floor/Building*', + 'Area/Sector/Locality*': 'Area/Sector/Locality*', + 'Nearby Landmark': 'Nearby Landmark', + 'Save Address As': 'Save Address As', + 'Enter address details': 'Enter address details', + 'Enter area/locality': 'Enter area/locality', + 'Add a landmark': 'Add a landmark', + 'Save Address': 'Save Address', + 'Please select Location': 'Please select Location', + 'Please Enter Flat / House / Floor / Building': 'Please Enter Flat / House / Floor / Building', + 'Please Enter Area / Sector / Locality': 'Please Enter Area / Sector / Locality', + 'Enable Location for a Personalized Experience': 'Enable Location for a Personalized Experience', + 'Allow location access to discover beauty stores and services near you.': 'Allow location access to discover beauty stores and services near you.', + 'Use current location': 'Use current location', + 'Set from map': 'Set from map', + 'Enter Manually location': 'Enter Manually location', + 'Search the store, item and more...': 'Search the store, item and more...', + 'Category': 'Category', + 'Highlights for you': 'Highlights for you', + 'New Arrivals': 'New Arrivals', + 'View All Arrivals': 'View All Arrivals', + 'Top Brands': 'Top Brands', + 'Brand': 'Brand', + 'Style up with the latest fits, now at unbeatable prices.': 'Style up with the latest fits, now at unbeatable prices.', + 'View All Products': 'View All Products', + 'All Store': 'All Store', + 'View All Stores': 'View All Stores', + 'Could not launch': 'Could not launch', + 'Highlights for you not found.': 'Highlights for you not found.', + 'Item Not available': 'Item Not available', + 'Service not available in this area': 'Service not available in this area', + 'Out of stock': 'Out of stock', + 'Variants': 'Variants', + 'Addons': 'Addons', + 'Delivery Type': 'Delivery Type', + 'Instant Delivery': 'Instant Delivery', + 'Standard': 'Standard', + 'instant': 'instant', + 'schedule': 'schedule', + 'Schedule Time': 'Schedule Time', + 'Your preferred time': 'Your preferred time', + 'Offers & Benefits': 'Offers & Benefits', + 'Apply Coupons': 'Apply Coupons', + 'Bill Details': 'Bill Details', + 'Item totals': 'Item totals', + 'Delivery Fee': 'Delivery Fee', + 'Free Delivery': 'Free Delivery', + 'Coupon Discount': 'Coupon Discount', + 'Special Discount': 'Special Discount', + 'Delivery Tips': 'Delivery Tips', + 'Remove': 'Remove', + 'To Pay': 'To Pay', + 'Thanks with a tip!': 'Thanks with a tip!', + 'Around the clock, our delivery partners make it happen. Show gratitude with a tip..': 'Around the clock, our delivery partners make it happen. Show gratitude with a tip..', + 'Other': 'Other', + 'Remarks': 'Remarks', + 'Write remarks for the store': 'Write remarks for the store', + 'Cashback Offer': 'Cashback Offer', + 'Cashback Name :': 'Cashback Name :', + 'You will get': 'You will get', + + 'Pay Via': 'Pay Via', + 'The total price must be greater than or equal to the coupon discount value for the code to apply. Please review your cart total.': + 'The total price must be greater than or equal to the coupon discount value for the code to apply. Please review your cart total.', + 'The total price must be greater than or equal to the special discount value for the code to apply. Please review your cart total.': + 'The total price must be greater than or equal to the special discount value for the code to apply. Please review your cart total.', + 'Tips Amount': 'Tips Amount', + 'Enter Tips Amount': 'Enter Tips Amount', + 'Add': 'Add', + 'Please enter tips Amount': 'Please enter tips Amount', + "You don't have sufficient wallet balance to place order": "You don't have sufficient wallet balance to place order", + 'This vendor has reached their maximum order capacity. Please select a different vendor or try again later.': + 'This vendor has reached their maximum order capacity. Please select a different vendor or try again later.', + 'Coupon Code': 'Coupon Code', + 'Enter coupon code': 'Enter coupon code', + 'Invalid Coupon': 'Invalid Coupon', + 'Apply': 'Apply', + 'Coupon code not applied': 'Coupon code not applied', + 'Order Placed': 'Order Placed', + 'Hang tight — your items are being delivered quickly and safely!': 'Hang tight — your items are being delivered quickly and safely!', + 'Order ID': 'Order ID', + 'Placing your order': 'Placing your order', + 'Take a moment to review your order before proceeding to checkout.': 'Take a moment to review your order before proceeding to checkout.', + 'Delivery Address': 'Delivery Address', + 'Track Order': 'Track Order', + 'Payment Option': 'Payment Option', + 'Cashback Offers': 'Cashback Offers', + 'Min spent': 'Min spent', + 'Maximum cashback up to': 'Maximum cashback up to', + 'Valid till': 'Valid till', + 'Change Language': 'Change Language', + 'Select your preferred language for a personalized app experience.': 'Select your preferred language for a personalized app experience.', + 'No Conversion found': 'No Conversion found', + 'Type message here....': 'Type message here....', + 'Send Media': 'Send Media', + 'Choose image from gallery': 'Choose image from gallery', + 'Choose video from gallery': 'Choose video from gallery', + 'Take a picture': 'Take a picture', + 'sent a message': 'sent a message', + 'Sent a video': 'Sent a video', + 'Sent a audio': 'Sent a audio', + 'Driver Inbox': 'Driver Inbox', + 'Store Inbox': 'Store Inbox', + 'Dine in Bookings': 'Dine in Bookings', + 'Order': 'Order', + 'Peoples': 'Peoples', + 'View in Map': 'View in Map', + 'Call Now': 'Call Now', + 'Booking Details': 'Booking Details', + 'Name': 'Name', + 'Phone number': 'Phone number', + 'Date and Time': 'Date and Time', + 'Guest': 'Guest', + 'Upcoming': 'Upcoming', + 'History': 'History', + 'Upcoming Booking not found.': 'Upcoming Booking not found.', + 'History not found.': 'History not found.', + 'Guest Number': 'Guest Number', + 'Book Table': 'Book Table', + 'Numbers of Guests': 'Numbers of Guests', + 'When are you visiting?': 'When are you visiting?', + 'Today': 'Today', + 'Tomorrow': 'Tomorrow', + 'Select time slot and scroll to see offers': 'Select time slot and scroll to see offers', + 'Special Occasion': 'Special Occasion', + 'Clear': 'Clear', + 'Is this your first visit?': 'Is this your first visit?', + 'Personal Details': 'Personal Details', + 'Additional Requests': 'Additional Requests', + 'Add message here....': 'Add message here....', + 'Book Now': 'Book Now', + 'Dine-In Request submitted successfully.': 'Dine-In Request submitted successfully.', + "Birthday": "Birthday", + "Anniversary": "Anniversary", + 'Ratings': 'Ratings', + 'Open': 'Open', + 'Close': 'Close', + 'View Timing': 'View Timing', + 'View Timings': 'View Timings', + 'Also applicable on food delivery': 'Also applicable on food delivery', + 'Please log in to the application. You are not logged in.': 'Please log in to the application. You are not logged in.', + 'Table Booking': 'Table Booking', + 'Quick Conformations': 'Quick Conformations', + 'Available food delivery': 'Available food delivery', + 'in 30-45 mins.': 'in 30-45 mins.', + 'Menu': 'Menu', + 'Location, Timing & Costs': 'Location, Timing & Costs', + 'View on Map': 'View on Map', + 'Timing': 'Timing', + 'To': 'To', + 'Cost for Two': 'Cost for Two', + 'for two': 'for two', + '(approx)': '(approx)', + 'Cuisines': 'Cuisines', + 'Dine-In Reservations': 'Dine-In Reservations', + 'Book a table at your favorite restaurant and enjoy a delightful dining experience.': 'Book a table at your favorite restaurant and enjoy a delightful dining experience.', + 'No Store Found in Your Area': 'No Store Found in Your Area', + 'Currently, there are no available store in your zone. Try changing your location to find nearby options.': + 'Currently, there are no available store in your zone. Try changing your location to find nearby options.', + 'Change Zone': 'Change Zone', + 'Explore the Categories': 'Explore the Categories', + 'View all': 'View all', + 'Popular Stores': 'Popular Stores', + 'All Stores': 'All Stores', + 'Categories': 'Categories', + 'Profile Information': 'Profile Information', + 'View and update your personal details, contact information, and preferences.': 'View and update your personal details, contact information, and preferences.', + 'First Name': 'First Name', + 'Last Name': 'Last Name', + 'Email': 'Email', + 'Phone Number': 'Phone Number', + 'Save Details': 'Save Details', + 'camera': 'camera', + 'gallery': 'gallery', + "failed_to_pick": "Failed to pick image", + 'Your Favourites, All in One Place': 'Your Favourites, All in One Place', + 'Please Log In to Continue': 'Please Log In to Continue', + "You’re not logged in. Please sign in to access your account and explore all features.": "You’re not logged in. Please sign in to access your account and explore all features.", + 'Favourite Store': 'Favourite Store', + 'Favourite Item': 'Favourite Item', + 'Favourite Store not found.': 'Favourite Store not found.', + 'Favourite Item not found.': 'Favourite Item not found.', + 'error': 'error', + 'Non Veg.': 'Non Veg.', + 'Pure veg.': 'Pure veg.', + 'Amount': 'Amount', + 'No worries!! We’ll send you reset instructions': 'No worries!! We’ll send you reset instructions', + 'Email Address': 'Email Address', + 'Enter email address': 'Enter email address', + 'Please enter valid email': 'Please enter valid email', + 'Customize Gift Card': 'Customize Gift Card', + 'Choose an amount': 'Choose an amount', + 'Enter gift card amount': 'Enter gift card amount', + 'Add Message (Optional)': 'Add Message (Optional)', + 'Please enter Amount': 'Please enter Amount', + 'Enter Amount': 'Enter Amount', + 'Complete payment and share this e-gift card with loved ones using any app': 'Complete payment and share this e-gift card with loved ones using any app', + 'Sub Total': 'Sub Total', + 'Grand Total': 'Grand Total', + 'Gift Card expire': 'Gift Card expire', + 'days after purchase': 'days after purchase', + "You don't have sufficient wallet balance to purchase gift card": "You don't have sufficient wallet balance to purchase gift card", + 'Gift card Purchases successfully': 'Gift card Purchases successfully', + 'Purchased Gift card not found': 'Purchased Gift card not found', + 'Gift Code': 'Gift Code', + 'Gift Pin': 'Gift Pin', + 'Share': 'Share', + 'Redeemed': 'Redeemed', + 'Not Redeem': 'Not Redeem', + 'Gift Code :': 'Gift Code :', + 'Gift Pin :': 'Gift Pin :', + 'Price :': 'Price :', + 'Expire Date :': 'Expire Date :', + 'Message': 'Message', + 'Redeem Gift Card': 'Redeem Gift Card', + 'Enter your gift card code to enjoy discounts and special offers on your orders.': 'Enter your gift card code to enjoy discounts and special offers on your orders.', + 'Enter gift code': 'Enter gift code', + 'Enter gift pin': 'Enter gift pin', + 'Redeem': 'Redeem', + 'Please Enter Gift Code': 'Please Enter Gift Code', + 'Please Enter Gift Pin': 'Please Enter Gift Pin', + 'Gift voucher already redeemed': 'Gift voucher already redeemed', + 'Gift Pin Invalid': 'Gift Pin Invalid', + 'Gift Voucher expire': 'Gift Voucher expire', + 'Voucher redeem successfully': 'Voucher redeem successfully', + 'Invalid Gift Code': 'Invalid Gift Code', + 'No Restaurant found': 'No Restaurant found', + "% off": "% OFF", + " off": " OFF", + 'Off': 'Off', + 'Search the restaurant, food and more...': 'Search the restaurant, food and more...', + 'Largest Discounts': 'Largest Discounts', + 'Delivery': 'Delivery', + 'TakeAway': 'TakeAway', + 'Do you really want to change the delivery option? Your cart will be empty.': 'Do you really want to change the delivery option? Your cart will be empty.', + 'Ok': 'Ok', + 'Upto': 'Upto', + "Error": "Error", + 'Search the dish, foo and more...': 'Search the dish, foo and more...', + 'See all': 'See all', + 'Our Categories': 'Our Categories', + 'Best Servings Food': 'Best Servings Food', + 'Large Discounts': 'Large Discounts', + 'Discounts Restaurants': 'Discounts Restaurants', + 'Save Upto 50% Off': 'Save Upto 50% Off', + '% OFF': '% OFF', + 'OFF': 'OFF', + 'Stories': 'Stories', + 'Best Food Stories Ever': 'Best Food Stories Ever', + 'Best Restaurants': 'Best Restaurants', + 'Live Tracking': 'Live Tracking', + 'Order Details': 'Order Details', + 'Order Delivered.': 'Order Delivered.', + 'Your Order has been Preparing and assign to the driver': 'Your Order has been Preparing and assign to the driver', + 'Preparation Time': 'Preparation Time', + 'Your Order': 'Your Order', + 'Rate us': 'Rate us', + 'Schedule': 'Schedule', + 'Payment Method': 'Payment Method', + 'Reorder': 'Reorder', + 'Item Added In a cart': 'Item Added In a cart', + 'You’re not logged in. Please sign in to access your account and explore all features': 'You’re not logged in. Please sign in to access your account and explore all features', + 'My Order': 'My Order', + 'Keep track your delivered, In Progress and Rejected item all in just one place.': 'Keep track your delivered, In Progress and Rejected item all in just one place.', + 'All': 'All', + 'In Progress': 'In Progress', + 'Delivered': 'Delivered', + 'Rejected': 'Rejected', + 'Order Not Found': 'Order Not Found', + 'View Details': 'View Details', + 'My Profile': 'My Profile', + 'Manage your personal information, preferences, and settings all in one place.': 'Manage your personal information, preferences, and settings all in one place.', + 'General Information': 'General Information', + 'Dine-In': 'Dine-In', + 'Gift Card': 'Gift Card', + 'Bookings Information': 'Bookings Information', + 'Dine-In Booking': 'Dine-In Booking', + 'Preferences': 'Preferences', + 'Dark Mode': 'Dark Mode', + 'Social': 'Social', + 'Refer a Friend': 'Refer a Friend', + 'Share app': 'Share app', + 'Check out Foodie, your ultimate food delivery application!': 'Check out Foodie, your ultimate food delivery application!', + 'Google Play:': 'Google Play:', + 'App Store:': 'App Store:', + 'Look what I made!': 'Look what I made!', + 'Rate the app': 'Rate the app', + 'Communication': 'Communication', + 'Provider Inbox': 'Provider Inbox', + 'Worker Inbox': 'Worker Inbox', + 'Top up Wallet': 'Top up Wallet', + 'Legal': 'Legal', + 'Terms & Conditions': 'Terms & Conditions', + 'Privacy Policy': 'Privacy Policy', + 'Terms and Conditions': 'Terms and Conditions', + 'Log out': 'Log out', + 'Are you sure you want to log out? You will need to enter your credentials to log back in.': 'Are you sure you want to log out? You will need to enter your credentials to log back in.', + 'Delete Account': 'Delete Account', + 'Are you sure you want to delete your account? This action is irreversible and will permanently remove all your data.': + 'Are you sure you want to delete your account? This action is irreversible and will permanently remove all your data.', + 'Delete': 'Delete', + 'Account deleted successfully': 'Account deleted successfully', + 'Contact Administrator': 'Contact Administrator', + 'V :': 'V :', + 'Cashbacks': 'Cashbacks', + 'Rate the item': 'Rate the item', + 'Choose a image and upload here': 'Choose a image and upload here', + 'JPEG, PNG': 'JPEG, PNG', + 'Brows Image': 'Brows Image', + 'Type comment': 'Type comment', + 'Submit Review': 'Submit Review', + 'Please Select': 'Please Select', + 'Camera': 'Camera', + 'Gallery': 'Gallery', + 'Failed to pick image:': 'Failed to pick image:', + 'Refer your friend and earn': 'Refer your friend and earn', + 'Invite Friends & Businesses': 'Invite Friends & Businesses', + 'Invite your friends to sign up with Foodie using your code, and you’ll earn': 'Invite your friends to sign up with Foodie using your code, and you’ll earn', + 'after their Success the first order!': 'after their Success the first order!', + 'Copied': 'Copied', + 'or': 'or', + 'Share Code': 'Share Code', + 'Hey there, thanks for choosing Foodie. Hope you love our product. If you do, share it with your friends using code': + 'Hey there, thanks for choosing Foodie. Hope you love our product. If you do, share it with your friends using code', + 'and get': 'and get', + 'when order completed': 'when order completed', + 'items': 'items', + 'View Cart': 'View Cart', + 'Timing is not added by store': 'Timing is not added by store', + 'Also applicable on table booking': 'Also applicable on table booking', + 'Additional Offers': 'Additional Offers', + 'Search the item and more...': 'Search the item and more...', + 'Veg': 'Veg', + 'Non Veg': 'Non Veg', + 'Info': 'Info', + "Product Information's": "Product Information's", + 'Gram': 'Gram', + 'Calories': 'Calories', + 'Proteins': 'Proteins', + 'Fats': 'Fats', + 'Specification': 'Specification', + 'Back': 'Back', + 'Required • Select any 1 option': 'Required • Select any 1 option', + 'Add item': 'Add item', + 'Reviews': 'Reviews', + 'reviews': 'reviews', + 'No Review found': 'No Review found', + 'Scan QR Code': 'Scan QR Code', + 'Store is not available': 'Store is not available', + 'km': 'km', + "Change": "Change", + 'Picked from Map': 'Picked from Map', + 'Allows users to view, manage, add, or edit delivery addresses.': 'Allows users to view, manage, add, or edit delivery addresses.', + "We'll be back soon!": "We'll be back soon!", + "Sorry for the inconvenience but we're performing some maintenance at the moment. We'll be back online shortly!": + "Sorry for the inconvenience but we're performing some maintenance at the moment. We'll be back online shortly!", + 'cashback after completing the order.': 'cashback after completing the order.', + 'Please enter coupon code': 'Please enter coupon code', + 'Pay': 'Pay', + 'after their Success the first order! 💸🍔': 'after their Success the first order! 💸🍔', + 'Find your favorite products and nearby stores': 'Find your favorite products and nearby stores', + 'Search Item & Store': 'Search Item & Store', + 'Search the store and item': 'Search the store and item', + 'Store': 'Store', + 'Select Top up Options': 'Select Top up Options', + 'Top-up': 'Top-up', + 'Please Enter minimum amount of': 'Please Enter minimum amount of', + 'My Wallet': 'My Wallet', + 'Keep track of your balance, transactions, and payment methods all in one place.': 'Keep track of your balance, transactions, and payment methods all in one place.', + 'Top up': 'Top up', + 'Transaction not found': 'Transaction not found', + 'Order details not available': 'Order details not available', + 'Favourite Services': 'Favourite Services', + 'Favourite Service not found.': 'Favourite Service not found.', + 'hr': 'hr', + 'Booking History': 'Booking History', + 'No ride found': 'No ride found', + 'OTP :': 'OTP :', + 'Date & Time': 'Date & Time', + 'Provider': 'Provider', + 'Start Time': 'Start Time', + 'End Time': 'End Time', + 'Worker': 'Worker', + 'Book Service': 'Book Service', + 'Services': 'Services', + 'Address': 'Address', + 'Description': 'Description', + 'Enter Description': 'Enter Description', + 'Booking Date & Slot': 'Booking Date & Slot', + 'Choose Date and Time': 'Choose Date and Time', + 'Price Detail': 'Price Detail', + 'Confirm': 'Confirm', + '% Off': '% Off', + 'valid till ': 'valid till ', + 'Redeem Your Coupons': 'Redeem Your Coupons', + 'Voucher or Coupon code': 'Voucher or Coupon code', + 'REDEEM NOW': 'REDEEM NOW', + 'Applied coupon not valid.': 'Applied coupon not valid.', + 'Price': 'Price', + 'Total Amount': 'Total Amount', + 'Explore services': 'Explore services', + 'Explore services tailored for you—quick, easy, and personalized.': 'Explore services tailored for you—quick, easy, and personalized.', + 'No Categories': 'No Categories', + 'About': 'About', + 'Review': 'Review', + 'No Image Found': 'No Image Found', + 'No review Found': 'No review Found', + 'Service Timing': 'Service Timing', + 'Start Time : ': 'Start Time : ', + 'End Time : ': 'End Time : ', + 'Service Days': 'Service Days', + 'No Service Found': 'No Service Found', + 'All Services': 'All Services', + 'Search Service': 'Search Service', + 'No Services Found': 'No Services Found', + 'Wallet balance is 0. Please recharge wallet.': 'Wallet balance is 0. Please recharge wallet.', + 'Insufficient wallet balance. Please add funds.': 'Insufficient wallet balance. Please add funds.', + 'Cancel Reason': 'Cancel Reason', + 'Booking ID': 'Booking ID', + 'Booking ID Copied': 'Booking ID Copied', + 'Booking Address :': 'Booking Address :', + 'Date:': 'Date:', + 'Time:': 'Time:', + 'About Worker': 'About Worker', + 'Call': 'Call', + 'Chat': 'Chat', + 'About provider': 'About provider', + 'Total Extra Charges : ': 'Total Extra Charges : ', + 'Extra charge Notes : ': 'Extra charge Notes : ', + 'New Date : ': 'New Date : ', + 'Cancel Booking': 'Cancel Booking', + 'Pay Extra Amount': 'Pay Extra Amount', + 'Please give reason for canceling this Booking': 'Please give reason for canceling this Booking', + 'Specify your reason here': 'Specify your reason here', + 'Please enter reason': 'Please enter reason', + 'Most Popular services': 'Most Popular services', + '"Total Amount': '"Total Amount', + 'Book Your Document Delivery': 'Book Your Document Delivery', + 'Schedule a secure and timely pickup & delivery': 'Schedule a secure and timely pickup & delivery', + 'Sender Information': 'Sender Information', + 'Receiver Information': 'Receiver Information', + 'Select delivery type': 'Select delivery type', + 'As soon as possible': 'As soon as possible', + 'Scheduled': 'Scheduled', + 'When to pickup at this address': 'When to pickup at this address', + 'Upload parcel image': 'Upload parcel image', + 'Upload Parcel Image': 'Upload Parcel Image', + 'Supported: .jpg, .jpeg, .png': 'Supported: .jpg, .jpeg, .png', + 'Max size 1MB': 'Max size 1MB', + 'Browse Image': 'Browse Image', + 'Your Location': 'Your Location', + 'Select parcel Weight': 'Select parcel Weight', + 'Normal': 'Normal', + 'Notes (Optional)': 'Notes (Optional)', + 'What are you sending?': 'What are you sending?', + 'Parcel History': 'Parcel History', + 'No orders found': 'No orders found', + 'Order Date:': 'Order Date:', + 'Pickup Address (Sender):': 'Pickup Address (Sender):', + 'Delivery Address (Receiver):': 'Delivery Address (Receiver):', + 'Your Order Has Been Placed!': 'Your Order Has Been Placed!', + 'We’ve received your parcel booking and it’s now being processed. You can track its status in real time.': + 'We’ve received your parcel booking and it’s now being processed. You can track its status in real time.', + 'Track Your Order': 'Track Your Order', + 'Order Confirmation': 'Order Confirmation', + 'Weight': 'Weight', + 'Rate': 'Rate', + 'Coupons': 'Coupons', + 'Write coupon code': 'Write coupon code', + 'Payment by': 'Payment by', + 'Sender': 'Sender', + 'Receiver': 'Receiver', + 'Insufficient wallet balance': 'Insufficient wallet balance', + 'Your parcel is on the way. Track it in real time below.': 'Your parcel is on the way. Track it in real time below.', + 'Schedule Pickup time:': 'Schedule Pickup time:', + 'Parcel Type:': 'Parcel Type:', + 'About Driver': 'About Driver', + 'Cancel Parcel': 'Cancel Parcel', + 'Parcel Status Timeline': 'Parcel Status Timeline', + 'No status updates yet': 'No status updates yet', + 'Rental History': 'Rental History', + 'Vehicle Type :': 'Vehicle Type :', + 'Package info :': 'Package info :', + 'Confirm Rent a Car': 'Confirm Rent a Car', + 'Your Preference': 'Your Preference', + 'Your current location': 'Your current location', + 'Please login to continue': 'Please login to continue', + 'Please select a vehicle type': 'Please select a vehicle type', + 'No preference available for the selected vehicle type': 'No preference available for the selected vehicle type', + 'Select Preferences': 'Select Preferences', + 'Booking Id :': 'Booking Id :', + 'Booking ID copied to clipboard': 'Booking ID copied to clipboard', + 'Rental Details': 'Rental Details', + 'Rental Package': 'Rental Package', + 'Rental Package Price': 'Rental Package Price', + 'Including': 'Including', + 'Including Hours': 'Including Hours', + 'Hr': 'Hr', + 'Extra': 'Extra', + 'Extra Minutes': 'Extra Minutes', + 'You are not able to pay now until driver adds kilometer': 'You are not able to pay now until driver adds kilometer', + 'OnDemand Service successfully booked': 'OnDemand Service successfully booked', + 'Booking Extra charge debited': 'Booking Extra charge debited', + 'Extra Charge Amount Credited': 'Extra Charge Amount Credited', + 'Failed to get access token': 'Failed to get access token', + 'Add your parcel image.': 'Add your parcel image.', + 'Please enter sender name': 'Please enter sender name', + 'Please enter sender mobile': 'Please enter sender mobile', + 'Please enter sender address': 'Please enter sender address', + 'Please enter receiver name': 'Please enter receiver name', + 'Please enter receiver mobile': 'Please enter receiver mobile', + 'Please enter receiver address': 'Please enter receiver address', + 'Please select scheduled date': 'Please select scheduled date', + 'Please select scheduled time': 'Please select scheduled time', + 'Please select parcel weight': 'Please select parcel weight', + 'Please select both sender and receiver locations': 'Please select both sender and receiver locations', + "Sender's location to receiver's location should be more than 1 km.": "Sender's location to receiver's location should be more than 1 km.", + "Something went wrong while booking.": "Something went wrong while booking.", + "Cab Amount debited": "Cab Amount debited", + "Stop": "Stop", + 'Departure': 'Departure', + 'Destination': 'Destination', + 'In Transit': 'In Transit', + 'Order Accepted': 'Order Accepted', + 'Driver Accepted': 'Driver Accepted', + 'Driver Pending': 'Driver Pending', + 'Order Shipped': 'Order Shipped', + 'Order Completed': 'Order Completed', + 'Order Rejected': 'Order Rejected', + 'Order Cancelled': 'Order Cancelled', + 'Driver Rejected': 'Driver Rejected', + 'You can only cancel before pickup.': 'You can only cancel before pickup.', + 'Refund for cancelled parcel order': 'Refund for cancelled parcel order', + 'success': 'success', + 'Order cancelled successfully': 'Order cancelled successfully', + 'Failed to cancel order:': 'Failed to cancel order:', + 'Parcel Amount debited': 'Parcel Amount debited', + 'Order placed successfully': 'Order placed successfully', + 'Initializing payment...': 'Initializing payment...', + 'Payment Unsuccessful!!': 'Payment Unsuccessful!!', + 'Rating saved successfully.': 'Rating saved successfully.', + 'Please add rate for food item.': 'Please add rate for food item.', + 'Placing booking...': 'Placing booking...', + 'Unable to fetch current location': 'Unable to fetch current location', + 'Failed to calculate total:': 'Failed to calculate total:', + 'Rental Amount debited': 'Rental Amount debited', + 'Refund for cancelled booking': 'Refund for cancelled booking', + 'Success': 'Success', + 'Booking cancelled successfully': 'Booking cancelled successfully', + 'Failed to cancel booking:': 'Failed to cancel booking:', + 'email': 'email', + 'Restaurants': 'Restaurants', + 'Amount Top-up successfully': 'Amount Top-up successfully', + 'Something want wrong please contact administrator': 'Something want wrong please contact administrator', + 'Payment': 'Payment', + 'Cancel Payment': 'Cancel Payment', + 'Cancel Payment?': 'Cancel Payment?', + 'Orange Money Payment': 'Orange Money Payment', + 'Are you sure you want to cancel this payment?': 'Are you sure you want to cancel this payment?', + 'No': 'No', + 'Yes': 'Yes', + 'Continue Payment': 'Continue Payment', + 'Exit': 'Exit', + 'Your cart already contains items from another restaurant. Would you like to replace them with items from this restaurant instead?': + 'Your cart already contains items from another restaurant. Would you like to replace them with items from this restaurant instead?', + 'Google map is not installed': 'Google map is not installed', + 'Google Go map is not installed': 'Google Go map is not installed', + 'Waze is not installed': 'Waze is not installed', + 'Mapswithme is not installed': 'Mapswithme is not installed', + 'YandexNavi is not installed': 'YandexNavi is not installed', + 'yandexMaps map is not installed': 'yandexMaps map is not installed', + 'Image failed to load.': 'Image failed to load.', + 'You denied location permission forever. Please allow location permission from your app settings and receive more accurate delivery.': + 'You denied location permission forever. Please allow location permission from your app settings and receive more accurate delivery.', + 'close': 'close', + 'Settings': 'Settings', + 'Move the map to select a location': 'Move the map to select a location', + 'Search place...': 'Search place...', + 'Confirm Location': 'Confirm Location', + 'PickUp Location': 'PickUp Location', + 'Search location...': 'Search location...', + 'Picked Location:': 'Picked Location:', + 'No Location Picked': 'No Location Picked', + 'Email is Required': 'Email is Required', + 'Invalid Email': 'Invalid Email', +}; diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..8e1f5ea --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,87 @@ +import 'package:customer/screen_ui/splash_screen/splash_screen.dart'; +import 'package:customer/service/localization_service.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/easy_loading_config.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import 'controllers/global_setting_controller.dart'; +import 'controllers/theme_controller.dart'; +import 'firebase_options.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(name: 'default', options: DefaultFirebaseOptions.currentPlatform); + + await Preferences.initPref(); + + Get.put(ThemeController()); + await configEasyLoading(); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + MyApp({super.key}); + + final themeController = Get.find(); + + @override + Widget build(BuildContext context) { + Get.put(ThemeController()); + return Obx( + () => GetMaterialApp( + debugShowCheckedModeBanner: false, + builder: (context, child) { + return SafeArea(bottom: true, top: false, child: EasyLoading.init()(context, child)); + }, + translations: LocalizationService(), + locale: LocalizationService.locale, + fallbackLocale: LocalizationService.locale, + themeMode: themeController.themeMode, + theme: ThemeData( + scaffoldBackgroundColor: AppThemeData.surface, + textTheme: TextTheme(bodyLarge: TextStyle(color: AppThemeData.grey900)), + appBarTheme: AppBarTheme( + backgroundColor: AppThemeData.surface, + foregroundColor: AppThemeData.grey900, + iconTheme: IconThemeData(color: AppThemeData.grey900), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: AppThemeData.surface, + selectedItemColor: AppThemeData.primary300, + unselectedItemColor: AppThemeData.grey600, + selectedLabelStyle: TextStyle(fontFamily: AppThemeData.bold, fontSize: 12), + unselectedLabelStyle: TextStyle(fontFamily: AppThemeData.bold, fontSize: 12), + type: BottomNavigationBarType.fixed, + ), + ), + darkTheme: ThemeData( + scaffoldBackgroundColor: AppThemeData.surfaceDark, + textTheme: TextTheme(bodyLarge: TextStyle(color: AppThemeData.greyDark900)), + appBarTheme: AppBarTheme( + backgroundColor: AppThemeData.surfaceDark, + foregroundColor: AppThemeData.greyDark900, + iconTheme: IconThemeData(color: AppThemeData.greyDark900), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: AppThemeData.grey900, + selectedItemColor: AppThemeData.primary300, + unselectedItemColor: AppThemeData.grey300, + selectedLabelStyle: TextStyle(fontFamily: AppThemeData.bold, fontSize: 12), + unselectedLabelStyle: TextStyle(fontFamily: AppThemeData.bold, fontSize: 12), + type: BottomNavigationBarType.fixed, + ), + ), + home: GetBuilder( + init: GlobalSettingController(), + builder: (context) { + return const SplashScreen(); + }, + ), + ), + ); + } +} diff --git a/lib/models/admin_commission_model.dart b/lib/models/admin_commission_model.dart new file mode 100644 index 0000000..ad11282 --- /dev/null +++ b/lib/models/admin_commission_model.dart @@ -0,0 +1,21 @@ +class AdminCommission { + String? amount; + bool? isEnabled; + String? commissionType; + + AdminCommission({this.amount, this.isEnabled, this.commissionType}); + + AdminCommission.fromJson(Map json) { + amount = json['commission'].toString(); + isEnabled = json['enable']; + commissionType = json['type']; + } + + Map toJson() { + final Map data = {}; + data['commission'] = amount; + data['enable'] = isEnabled; + data['type'] = commissionType; + return data; + } +} diff --git a/lib/models/advertisement_model.dart b/lib/models/advertisement_model.dart new file mode 100644 index 0000000..36ef093 --- /dev/null +++ b/lib/models/advertisement_model.dart @@ -0,0 +1,97 @@ + +import 'package:cloud_firestore/cloud_firestore.dart'; + +class AdvertisementModel { + String? coverImage; + Timestamp? createdAt; + String? description; + Timestamp? endDate; + String? id; + bool? paymentStatus; + String? priority; + String? profileImage; + bool? showRating; + bool? showReview; + Timestamp? startDate; + String? status; + String? title; + String? type; + String? vendorId; + String? video; + bool? isPaused; + Timestamp? updatedAt; + String? canceledNote; + String? pauseNote; + + AdvertisementModel({ + this.coverImage, + this.createdAt, + this.description, + this.endDate, + this.id, + this.paymentStatus, + this.priority, + this.profileImage, + this.showRating, + this.showReview, + this.startDate, + this.status, + this.title, + this.type, + this.vendorId, + this.video, + this.isPaused, + this.updatedAt, + this.canceledNote, + this.pauseNote, + }); + + factory AdvertisementModel.fromJson(Map json) { + return AdvertisementModel( + coverImage: json['coverImage'], + createdAt: json['createdAt'], + description: json['description'], + endDate: json['endDate'], + id: json['id'], + paymentStatus: json['paymentStatus'], + priority: json['priority'], + profileImage: json['profileImage'], + showRating: json['showRating'], + showReview: json['showReview'], + startDate: json['startDate'], + status: json['status'], + title: json['title'], + type: json['type'], + vendorId: json['vendorId'], + video: json['video'], + isPaused: json['isPaused'], + updatedAt: json['updatedAt'], + canceledNote: json['canceledNote'], + pauseNote: json['pauseNote']); + } + + Map toJson() { + return { + 'coverImage': coverImage, + 'createdAt': createdAt, + 'description': description, + 'endDate': endDate, + 'id': id, + 'paymentStatus': paymentStatus, + 'priority': priority, + 'profileImage': profileImage, + 'showRating': showRating, + 'showReview': showReview, + 'startDate': startDate, + 'status': status, + 'title': title, + 'type': type, + 'vendorId': vendorId, + 'video': video, + 'isPaused': isPaused, + 'updatedAt': updatedAt, + 'canceledNote': canceledNote, + 'pauseNote': pauseNote, + }; + } +} diff --git a/lib/models/attributes_model.dart b/lib/models/attributes_model.dart new file mode 100644 index 0000000..8b40b92 --- /dev/null +++ b/lib/models/attributes_model.dart @@ -0,0 +1,18 @@ +class AttributesModel { + String? id; + String? title; + + AttributesModel({this.id, this.title}); + + AttributesModel.fromJson(Map json) { + id = json['id']; + title = json['title']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + return data; + } +} diff --git a/lib/models/banner_model.dart b/lib/models/banner_model.dart new file mode 100644 index 0000000..f1d84b6 --- /dev/null +++ b/lib/models/banner_model.dart @@ -0,0 +1,39 @@ +class BannerModel { + String? id; + int? setOrder; + String? position; + String? sectionId; + String? photo; + String? title; + String? redirect_type; + String? redirect_id; + bool? isPublish; + + BannerModel({this.id, this.setOrder, this.position, this.redirect_type, this.redirect_id, this.sectionId, this.photo, this.title, this.isPublish}); + + BannerModel.fromJson(Map json) { + id = json['id']; + position = json['position']; + sectionId = json['sectionId']; + setOrder = json['set_order']; + photo = json['photo']; + title = json['title']; + isPublish = json['is_publish']; + redirect_type = json['redirect_type']; + redirect_id = json['redirect_id']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['position'] = position; + data['sectionId'] = sectionId; + data['set_order'] = setOrder; + data['photo'] = photo; + data['title'] = title; + data['is_publish'] = isPublish; + data['redirect_type'] = redirect_type; + data['redirect_id'] = redirect_id; + return data; + } +} diff --git a/lib/models/brands_model.dart b/lib/models/brands_model.dart new file mode 100644 index 0000000..29dd5dd --- /dev/null +++ b/lib/models/brands_model.dart @@ -0,0 +1,27 @@ +class BrandsModel { + String? photo; + String? sectionId; + String? id; + String? title; + bool? isPublish; + + BrandsModel({this.photo, this.sectionId, this.id, this.title, this.isPublish}); + + BrandsModel.fromJson(Map json) { + photo = json['photo']; + sectionId = json['sectionId']; + id = json['id']; + title = json['title']; + isPublish = json['is_publish']; + } + + Map toJson() { + final Map data = {}; + data['photo'] = photo; + data['sectionId'] = sectionId; + data['id'] = id; + data['title'] = title; + data['is_publish'] = isPublish; + return data; + } +} diff --git a/lib/models/cab_order_model.dart b/lib/models/cab_order_model.dart new file mode 100644 index 0000000..1b666ae --- /dev/null +++ b/lib/models/cab_order_model.dart @@ -0,0 +1,192 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vehicle_type.dart'; + +class CabOrderModel { + String? status; + List? rejectedByDrivers; + String? couponId; + Timestamp? scheduleDateTime; + String? duration; + bool? roundTrip; + bool? paymentStatus; + String? discount; + String? destinationLocationName; + String? authorID; + Timestamp? createdAt; + DestinationLocation? destinationLocation; + String? adminCommissionType; + String? sourceLocationName; + String? rideType; + List? taxSetting; + Timestamp? triggerDelevery; + String? id; + String? adminCommission; + String? couponCode; + Timestamp? scheduleReturnDateTime; + String? sectionId; + String? tipAmount; + String? distance; + String? vehicleId; + String? paymentMethod; + VehicleType? vehicleType; + String? otpCode; + DestinationLocation? sourceLocation; + UserModel? author; + UserModel? driver; + String? driverId; + String? subTotal; + + CabOrderModel({ + this.status, + this.rejectedByDrivers, + this.scheduleDateTime, + this.duration, + this.roundTrip, + this.paymentStatus, + this.discount, + this.destinationLocationName, + this.authorID, + this.createdAt, + this.destinationLocation, + this.adminCommissionType, + this.sourceLocationName, + this.rideType, + this.taxSetting, + this.triggerDelevery, + this.id, + this.adminCommission, + this.couponCode, + this.couponId, + this.scheduleReturnDateTime, + this.sectionId, + this.tipAmount, + this.distance, + this.vehicleId, + this.paymentMethod, + this.vehicleType, + this.otpCode, + this.sourceLocation, + this.author, + this.subTotal, + this.driver, + this.driverId, + }); + + CabOrderModel.fromJson(Map json) { + status = json['status']; + rejectedByDrivers = json['rejectedByDrivers'] ?? []; + couponId = json['couponId']; + scheduleDateTime = json['scheduleDateTime']; + duration = json['duration']; + roundTrip = json['roundTrip']; + paymentStatus = json['paymentStatus']; + discount = json['discount'] == null ?"0.0": json['discount'].toString(); + destinationLocationName = json['destinationLocationName']; + authorID = json['authorID']; + createdAt = json['createdAt']; + destinationLocation = json['destinationLocation'] != null ? DestinationLocation.fromJson(json['destinationLocation']) : null; + adminCommissionType = json['adminCommissionType']; + sourceLocationName = json['sourceLocationName']; + rideType = json['rideType']; + if (json['taxSetting'] != null) { + taxSetting = []; + json['taxSetting'].forEach((v) { + taxSetting!.add(TaxModel.fromJson(v)); + }); + } + triggerDelevery = json['trigger_delevery']; + id = json['id']; + adminCommission = json['adminCommission']; + couponCode = json['couponCode']; + scheduleReturnDateTime = json['scheduleReturnDateTime']; + sectionId = json['sectionId']; + tipAmount = json['tip_amount']; + distance = json['distance']; + vehicleId = json['vehicleId']; + paymentMethod = json['paymentMethod']; + vehicleType = json['vehicleType'] != null ? VehicleType.fromJson(json['vehicleType']) : null; + otpCode = json['otpCode']; + sourceLocation = json['sourceLocation'] != null ? DestinationLocation.fromJson(json['sourceLocation']) : null; + author = json['author'] != null ? UserModel.fromJson(json['author']) : null; + subTotal = json['subTotal']; + driver = json['driver'] != null ? UserModel.fromJson(json['driver']) : null; + driverId = json['driverId']; + } + + Map toJson() { + final Map data = {}; + data['status'] = status; + // if (rejectedByDrivers != null) { + // data['rejectedByDrivers'] = rejectedByDrivers!.map((v) => v.toJson()).toList(); + // } + if (rejectedByDrivers != null) { + data['rejectedByDrivers'] = rejectedByDrivers; + } + data['couponId'] = couponId; + data['scheduleDateTime'] = scheduleDateTime; + data['duration'] = duration; + data['roundTrip'] = roundTrip; + data['paymentStatus'] = paymentStatus; + data['discount'] = discount; + data['destinationLocationName'] = destinationLocationName; + data['authorID'] = authorID; + data['createdAt'] = createdAt; + if (destinationLocation != null) { + data['destinationLocation'] = destinationLocation!.toJson(); + } + data['adminCommissionType'] = adminCommissionType; + data['sourceLocationName'] = sourceLocationName; + data['rideType'] = rideType; + if (taxSetting != null) { + data['taxSetting'] = taxSetting!.map((v) => v.toJson()).toList(); + } + data['trigger_delevery'] = triggerDelevery!; + data['id'] = id; + data['adminCommission'] = adminCommission; + data['couponCode'] = couponCode; + data['scheduleReturnDateTime'] = scheduleReturnDateTime; + data['sectionId'] = sectionId; + data['tip_amount'] = tipAmount; + data['distance'] = distance; + data['vehicleId'] = vehicleId; + data['paymentMethod'] = paymentMethod; + data['driverId'] = driverId; + if (driver != null) { + data['driver'] = driver!.toJson(); + } + + if (vehicleType != null) { + data['vehicleType'] = vehicleType!.toJson(); + } + data['otpCode'] = otpCode; + if (sourceLocation != null) { + data['sourceLocation'] = sourceLocation!.toJson(); + } + if (author != null) { + data['author'] = author!.toJson(); + } + data['subTotal'] = subTotal; + return data; + } +} + +class DestinationLocation { + double? longitude; + double? latitude; + + DestinationLocation({this.longitude, this.latitude}); + + DestinationLocation.fromJson(Map json) { + longitude = json['longitude']; + latitude = json['latitude']; + } + + Map toJson() { + final Map data = {}; + data['longitude'] = longitude; + data['latitude'] = latitude; + return data; + } +} diff --git a/lib/models/cart_product_model.dart b/lib/models/cart_product_model.dart new file mode 100644 index 0000000..21cc492 --- /dev/null +++ b/lib/models/cart_product_model.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; + +class CartProductModel { + String? id; + String? categoryId; + String? name; + String? photo; + String? price; + String? discountPrice; + String? vendorID; + int? quantity; + String? extrasPrice; + List? extras; + VariantInfo? variantInfo; + + CartProductModel({ + this.id, + this.categoryId, + this.name, + this.photo, + this.price, + this.discountPrice, + this.vendorID, + this.quantity, + this.extrasPrice, + this.variantInfo, + this.extras, + }); + + CartProductModel.fromJson(Map json) { + id = json['id']; + categoryId = json['category_id']; + name = json['name']; + photo = json['photo']; + price = json['price'] ?? "0.0"; + discountPrice = json['discountPrice'] ?? "0.0"; + vendorID = json['vendorID']; + quantity = json['quantity']; + extrasPrice = json['extras_price']; + + extras = json['extras'] == "null" || json['extras'] == null + ? null + : "String" == json['extras'].runtimeType.toString() + ? List.from(jsonDecode(json['extras'])) + : List.from(json['extras']); + + variantInfo = json['variant_info'] == "null" || json['variant_info'] == null + ? null + : "String" == json['variant_info'].runtimeType.toString() + ? VariantInfo.fromJson(jsonDecode(json['variant_info'])) + : VariantInfo.fromJson(json['variant_info']); + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['category_id'] = categoryId; + data['name'] = name; + data['photo'] = photo; + data['price'] = price; + data['discountPrice'] = discountPrice; + data['vendorID'] = vendorID; + data['quantity'] = quantity; + data['extras_price'] = extrasPrice; + data['extras'] = extras; + if (variantInfo != null) { + data['variant_info'] = variantInfo?.toJson(); // Handle null value + } + return data; + } +} + +class VariantInfo { + String? variantId; + String? variantPrice; + String? variantSku; + String? variantImage; + Map? variantOptions; + + VariantInfo({this.variantId, this.variantPrice, this.variantSku, this.variantImage, this.variantOptions}); + + VariantInfo.fromJson(Map json) { + variantId = json['variantId'] ?? ''; + variantPrice = json['variantPrice'] ?? ''; + variantSku = json['variantSku'] ?? ''; + variantImage = json['variant_image'] ?? ''; + variantOptions = json['variant_options'] ?? {}; + } + + Map toJson() { + final Map data = {}; + data['variantId'] = variantId; + data['variantPrice'] = variantPrice; + data['variantSku'] = variantSku; + data['variant_image'] = variantImage; + data['variant_options'] = variantOptions; + return data; + } +} diff --git a/lib/models/cashback_model.dart b/lib/models/cashback_model.dart new file mode 100644 index 0000000..72af664 --- /dev/null +++ b/lib/models/cashback_model.dart @@ -0,0 +1,76 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class CashbackModel { + bool? allCustomer; + bool? allPayment; + double? cashbackAmount; + String? cashbackType; + List? customerIds; + Timestamp? endDate; + String? id; + bool? isEnabled; + double? maximumDiscount; + double? minimumPurchaseAmount; + List? paymentMethods; + int? redeemLimit; + Timestamp? startDate; + String? title; + double? cashbackValue; + + CashbackModel({ + this.allCustomer, + this.allPayment, + this.cashbackAmount, + this.cashbackValue, + this.cashbackType, + this.customerIds, + this.endDate, + this.id, + this.isEnabled, + this.maximumDiscount, + this.minimumPurchaseAmount, + this.paymentMethods, + this.redeemLimit, + this.startDate, + this.title, + }); + + factory CashbackModel.fromJson(Map json) { + return CashbackModel( + allCustomer: json['allCustomer'], + allPayment: json['allPayment'], + cashbackAmount: (json['cashbackAmount'] != null) ? double.tryParse(json['cashbackAmount'].toString()) : null, + cashbackValue: (json['cashbackValue'] != null) ? double.tryParse(json['cashbackValue'].toString()) : null, + cashbackType: json['cashbackType'] ?? '', + customerIds: json['customerIds'] != null ? List.from(json['customerIds']) : null, + endDate: json['endDate'] is Timestamp ? json['endDate'] as Timestamp : null, + id: json['id'], + isEnabled: json['isEnabled'], + maximumDiscount: (json['maximumDiscount'] != null) ? double.tryParse(json['maximumDiscount'].toString()) : null, + minimumPurchaseAmount: (json['minumumPurchaseAmount'] != null) ? double.tryParse(json['minumumPurchaseAmount'].toString()) : null, + paymentMethods: json['paymentMethods'] != null ? List.from(json['paymentMethods']) : null, + redeemLimit: int.parse("${json['redeemLimit'] ?? 0}"), + startDate: json['startDate'] is Timestamp ? json['startDate'] as Timestamp : null, + title: json['title'], + ); + } + Map toJson() { + return { + 'allCustomer': allCustomer, + 'allPayment': allPayment, + 'cashbackAmount': cashbackAmount, + if (cashbackValue != null) 'cashbackValue': cashbackValue, + 'cashbackType': cashbackType, + 'customerIds': customerIds, + 'endDate': endDate, + 'id': id, + 'isEnabled': isEnabled, + 'maximumDiscount': maximumDiscount, + 'minumumPurchaseAmount': minimumPurchaseAmount, + 'paymentMethods': paymentMethods, + 'redeemLimit': redeemLimit, + 'startDate': startDate, + 'title': title, + }; + } +} diff --git a/lib/models/cashback_redeem_model.dart b/lib/models/cashback_redeem_model.dart new file mode 100644 index 0000000..380092e --- /dev/null +++ b/lib/models/cashback_redeem_model.dart @@ -0,0 +1,37 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class CashbackRedeemModel { + final String? id; + final String? cashbackId; + final String? userId; + final String? orderId; + final Timestamp? createdAt; + + CashbackRedeemModel({ + this.id, + this.cashbackId, + this.userId, + this.orderId, + this.createdAt, + }); + + factory CashbackRedeemModel.fromJson(Map json) { + return CashbackRedeemModel( + id: json['id'], + cashbackId: json['cashbackId'], + userId: json['userId'], + orderId: json['orderId'], + createdAt: json['createdAt'] == null ? null : json['createdAt'] as Timestamp, + ); + } + + Map toJson() { + return { + 'id': id, + 'cashbackId': cashbackId, + 'userId': userId, + 'orderId': orderId, + 'createdAt': createdAt, + }; + } +} diff --git a/lib/models/category_model.dart b/lib/models/category_model.dart new file mode 100644 index 0000000..20818a5 --- /dev/null +++ b/lib/models/category_model.dart @@ -0,0 +1,24 @@ +class CategoryModel { + String? id; + String? title; + String? image; + bool? publish; + + CategoryModel({this.id, this.title, this.image, this.publish}); + + CategoryModel.fromJson(Map json) { + id = json['id'] ?? ''; + title = json['title'] ?? ''; + image = json['image'] ?? ''; + publish = json['publish']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + data['image'] = image; + data['publish'] = publish; + return data; + } +} diff --git a/lib/models/conversation_model.dart b/lib/models/conversation_model.dart new file mode 100644 index 0000000..67ec38c --- /dev/null +++ b/lib/models/conversation_model.dart @@ -0,0 +1,75 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class ConversationModel { + String? id; + String? senderId; + String? receiverId; + String? orderId; + String? message; + String? messageType; + String? videoThumbnail; + Url? url; + Timestamp? createdAt; + + ConversationModel({ + this.id, + this.senderId, + this.receiverId, + this.orderId, + this.message, + this.messageType, + this.videoThumbnail, + this.url, + this.createdAt, + }); + + factory ConversationModel.fromJson(Map parsedJson) { + return ConversationModel( + id: parsedJson['id'] ?? '', + senderId: parsedJson['senderId'] ?? '', + receiverId: parsedJson['receiverId'] ?? '', + orderId: parsedJson['orderId'] ?? '', + message: parsedJson['message'] ?? '', + messageType: parsedJson['messageType'] ?? '', + videoThumbnail: parsedJson['videoThumbnail'] ?? '', + url: parsedJson.containsKey('url') + ? parsedJson['url'] != null + ? Url.fromJson(parsedJson['url']) + : null + : Url(), + createdAt: parsedJson['createdAt'] ?? Timestamp.now(), + ); + } + + Map toJson() { + return { + 'id': id, + 'senderId': senderId, + 'receiverId': receiverId, + 'orderId': orderId, + 'message': message, + 'messageType': messageType, + 'videoThumbnail': videoThumbnail, + 'url': url?.toJson(), + 'createdAt': createdAt, + }; + } +} + +class Url { + String mime; + + String url; + + String? videoThumbnail; + + Url({this.mime = '', this.url = '', this.videoThumbnail}); + + factory Url.fromJson(Map parsedJson) { + return Url(mime: parsedJson['mime'] ?? '', url: parsedJson['url'] ?? '', videoThumbnail: parsedJson['videoThumbnail'] ?? ''); + } + + Map toJson() { + return {'mime': mime, 'url': url, 'videoThumbnail': videoThumbnail}; + } +} diff --git a/lib/models/coupon_model.dart b/lib/models/coupon_model.dart new file mode 100644 index 0000000..e970ff7 --- /dev/null +++ b/lib/models/coupon_model.dart @@ -0,0 +1,50 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class CouponModel { + String? discountType; + String? id; + String? code; + String? discount; + String? image; + Timestamp? expiresAt; + Timestamp? createdAt; + String? description; + String? sectionId; + bool? isPublic; + String? vendorID; + bool? isEnabled; + + CouponModel({this.discountType, this.id, this.code, this.discount, this.image, this.expiresAt, this.description, this.isPublic, this.vendorID, this.isEnabled,this.createdAt,this.sectionId}); + + CouponModel.fromJson(Map json) { + discountType = json['discountType']; + id = json['id']; + code = json['code']; + discount = json['discount']; + image = json['image']; + expiresAt = json['expiresAt']; + description = json['description']; + isPublic = json['isPublic']; + vendorID = json['vendorID']; + isEnabled = json['isEnabled']; + createdAt = json['createdAt']; + sectionId = json['section_id']; + } + + Map toJson() { + final Map data = {}; + data['discountType'] = discountType; + data['id'] = id; + data['code'] = code; + data['discount'] = discount; + data['image'] = image; + data['expiresAt'] = expiresAt; + data['description'] = description; + data['isPublic'] = isPublic; + data['vendorID'] = vendorID; + data['isEnabled'] = isEnabled; + data['createdAt'] = createdAt; + data['section_id'] = sectionId; + return data; + } +} diff --git a/lib/models/currency_model.dart b/lib/models/currency_model.dart new file mode 100644 index 0000000..70fb724 --- /dev/null +++ b/lib/models/currency_model.dart @@ -0,0 +1,48 @@ +class CurrencyModel { + String code; + int decimal; + String id; + bool isactive; + num rounding; + String name; + String symbol; + bool symbolatright; + + CurrencyModel({ + this.code = '', + this.decimal = 0, + this.isactive = false, + this.id = '', + this.name = '', + this.rounding = 0, + this.symbol = '', + this.symbolatright = false, + }); + + factory CurrencyModel.fromJson(Map parsedJson) { + return CurrencyModel( + code: parsedJson['code'] ?? '', + decimal: parsedJson['decimal_degits'] ?? 0, + isactive: parsedJson['isActive'] ?? '', + id: parsedJson['id'] ?? '', + name: parsedJson['name'] ?? '', + rounding: parsedJson['rounding'] ?? 0, + symbol: parsedJson['symbol'] ?? '', + symbolatright: parsedJson['symbolAtRight'] ?? '', + ); + } + + + Map toJson() { + return { + 'code': code, + 'decimal_degits': decimal, + 'isActive': isactive, + 'rounding': rounding, + 'id': id, + 'name': name, + 'symbol': symbol, + 'symbolAtRight': symbolatright, + }; + } +} diff --git a/lib/models/dine_in_booking_model.dart b/lib/models/dine_in_booking_model.dart new file mode 100644 index 0000000..973a6c1 --- /dev/null +++ b/lib/models/dine_in_booking_model.dart @@ -0,0 +1,93 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; + +class DineInBookingModel { + String? discount; + String? id; + String? guestPhone; + String? guestFirstName; + String? status; + UserModel? author; + String? guestEmail; + String? vendorID; + String? occasion; + String? authorID; + String? specialRequest; + Timestamp? date; + String? totalGuest; + VendorModel? vendor; + bool? firstVisit; + Timestamp? createdAt; + String? guestLastName; + String? discountType; + + DineInBookingModel( + {this.discount, + this.id, + this.guestPhone, + this.guestFirstName, + this.status, + this.author, + this.guestEmail, + this.vendorID, + this.occasion, + this.authorID, + this.specialRequest, + this.date, + this.totalGuest, + this.vendor, + this.firstVisit, + this.createdAt, + this.guestLastName, + this.discountType}); + + DineInBookingModel.fromJson(Map json) { + print(json['id']); + discount = json['discount']; + id = json['id']; + guestPhone = json['guestPhone']; + guestFirstName = json['guestFirstName']; + status = json['status']; + author = json['author'] != null ? UserModel.fromJson(json['author']) : null; + guestEmail = json['guestEmail']; + vendorID = json['vendorID']; + occasion = json['occasion']; + authorID = json['authorID']; + specialRequest = json['specialRequest']; + date = json['date']; + totalGuest = json['totalGuest'].toString(); + vendor = json['vendor'] != null ? VendorModel.fromJson(json['vendor']) : null; + firstVisit = json['firstVisit']; + createdAt = json['createdAt']; + guestLastName = json['guestLastName']; + discountType = json['discountType']; + } + + Map toJson() { + final Map data = {}; + data['discount'] = discount; + data['id'] = id; + data['guestPhone'] = guestPhone; + data['guestFirstName'] = guestFirstName; + data['status'] = status; + if (author != null) { + data['author'] = author!.toJson(); + } + data['guestEmail'] = guestEmail; + data['vendorID'] = vendorID; + data['occasion'] = occasion; + data['authorID'] = authorID; + data['specialRequest'] = specialRequest; + data['date'] = date; + data['totalGuest'] = totalGuest; + if (vendor != null) { + data['vendor'] = vendor!.toJson(); + } + data['firstVisit'] = firstVisit; + data['createdAt'] = createdAt; + data['guestLastName'] = guestLastName; + data['discountType'] = discountType; + return data; + } +} diff --git a/lib/models/email_template_model.dart b/lib/models/email_template_model.dart new file mode 100644 index 0000000..4d83249 --- /dev/null +++ b/lib/models/email_template_model.dart @@ -0,0 +1,27 @@ +class EmailTemplateModel { + String? id; + String? type; + String? message; + String? subject; + bool? isSendToAdmin; + + EmailTemplateModel({this.subject, this.id, this.type, this.message, this.isSendToAdmin}); + + EmailTemplateModel.fromJson(Map json) { + subject = json['subject']; + id = json['id']; + type = json['type']; + message = json['message']; + isSendToAdmin = json['isSendToAdmin']; + } + + Map toJson() { + final Map data = {}; + data['subject'] = subject; + data['id'] = id; + data['type'] = type; + data['message'] = message; + data['isSendToAdmin'] = isSendToAdmin; + return data; + } +} diff --git a/lib/models/favorite_ondemand_service_model.dart b/lib/models/favorite_ondemand_service_model.dart new file mode 100644 index 0000000..a614751 --- /dev/null +++ b/lib/models/favorite_ondemand_service_model.dart @@ -0,0 +1,20 @@ +class FavouriteOndemandServiceModel { + String? serviceAuthorId; + String? service_id; + String? user_id; + String? section_id; + + FavouriteOndemandServiceModel({this.service_id, this.serviceAuthorId, this.user_id, this.section_id}); + + factory FavouriteOndemandServiceModel.fromJson(Map parsedJson) { + return FavouriteOndemandServiceModel( + serviceAuthorId: parsedJson["service_author_id"] ?? "", + section_id: parsedJson["section_id"] ?? "", + user_id: parsedJson["user_id"] ?? "", + service_id: parsedJson["service_id"] ?? ""); + } + + Map toJson() { + return {"service_author_id": serviceAuthorId, "section_id": section_id, "user_id": user_id, "service_id": service_id}; + } +} diff --git a/lib/models/favourite_item_model.dart b/lib/models/favourite_item_model.dart new file mode 100644 index 0000000..537c03e --- /dev/null +++ b/lib/models/favourite_item_model.dart @@ -0,0 +1,21 @@ +class FavouriteItemModel { + String? storeId; + String? userId; + String? productId; + String? sectionId; + + FavouriteItemModel({this.storeId, this.userId, this.productId, this.sectionId}); + + factory FavouriteItemModel.fromJson(Map parsedJson) { + return FavouriteItemModel( + storeId: parsedJson["store_id"] ?? "", + userId: parsedJson["user_id"] ?? "", + productId: parsedJson["product_id"] ?? "", + sectionId: parsedJson["section_id"] ?? "", + ); + } + + Map toJson() { + return {"store_id": storeId, "user_id": userId, "product_id": productId, "section_id": sectionId}; + } +} diff --git a/lib/models/favourite_model.dart b/lib/models/favourite_model.dart new file mode 100644 index 0000000..0537ed6 --- /dev/null +++ b/lib/models/favourite_model.dart @@ -0,0 +1,15 @@ +class FavouriteModel { + String? restaurantId; + String? userId; + String? sectionId; + + FavouriteModel({this.restaurantId, this.userId,this.sectionId}); + + factory FavouriteModel.fromJson(Map parsedJson) { + return FavouriteModel(restaurantId: parsedJson["store_id"] ?? "", userId: parsedJson["user_id"] ?? "",sectionId: parsedJson["section_id"] ?? ""); + } + + Map toJson() { + return {"store_id": restaurantId, "user_id": userId, "section_id": sectionId}; + } +} diff --git a/lib/models/gift_cards_model.dart b/lib/models/gift_cards_model.dart new file mode 100644 index 0000000..fce84cf --- /dev/null +++ b/lib/models/gift_cards_model.dart @@ -0,0 +1,36 @@ + +import 'package:cloud_firestore/cloud_firestore.dart'; + +class GiftCardsModel { + Timestamp? createdAt; + String? image; + String? expiryDay; + String? id; + String? message; + String? title; + bool? isEnable; + + GiftCardsModel({this.createdAt, this.image, this.expiryDay, this.id, this.message, this.title, this.isEnable}); + + GiftCardsModel.fromJson(Map json) { + createdAt = json['createdAt']; + image = json['image']; + expiryDay = json['expiryDay']; + id = json['id']; + message = json['message']; + title = json['title']; + isEnable = json['isEnable']; + } + + Map toJson() { + final Map data = {}; + data['createdAt'] = createdAt; + data['image'] = image; + data['expiryDay'] = expiryDay; + data['id'] = id; + data['message'] = message; + data['title'] = title; + data['isEnable'] = isEnable; + return data; + } +} diff --git a/lib/models/gift_cards_order_model.dart b/lib/models/gift_cards_order_model.dart new file mode 100644 index 0000000..f97aa19 --- /dev/null +++ b/lib/models/gift_cards_order_model.dart @@ -0,0 +1,52 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class GiftCardsOrderModel { + String? price; + Timestamp? expireDate; + Timestamp? createdDate; + String? message; + String? id; + String? giftId; + String? giftTitle; + String? giftCode; + String? giftPin; + bool? redeem; + String? paymentType; + String? userid; + bool? isPasswordShow; + + GiftCardsOrderModel( + {this.price, this.id, this.expireDate, this.createdDate, this.giftTitle, this.message, this.giftId, this.giftCode, this.giftPin, this.redeem, this.paymentType, this.userid}); + + GiftCardsOrderModel.fromJson(Map json) { + id = json['id']; + price = json['price']; + expireDate = json['expireDate']; + createdDate = json['createdDate']; + giftTitle = json['giftTitle']; + message = json['message']; + giftId = json['giftId']; + giftCode = json['giftCode']; + giftPin = json['giftPin']; + redeem = json['redeem']; + paymentType = json['paymentType']; + userid = json['userid']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['price'] = price; + data['expireDate'] = expireDate; + data['createdDate'] = createdDate; + data['message'] = message; + data['giftId'] = giftId; + data['giftTitle'] = giftTitle; + data['giftCode'] = giftCode; + data['giftPin'] = giftPin; + data['redeem'] = redeem; + data['paymentType'] = paymentType; + data['userid'] = userid; + return data; + } +} diff --git a/lib/models/inbox_model.dart b/lib/models/inbox_model.dart new file mode 100644 index 0000000..211a965 --- /dev/null +++ b/lib/models/inbox_model.dart @@ -0,0 +1,61 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class InboxModel { + String? customerId; + String? customerName; + String? customerProfileImage; + String? lastMessage; + String? orderId; + String? restaurantId; + String? restaurantName; + String? restaurantProfileImage; + String? lastSenderId; + String? chatType; + Timestamp? createdAt; + + InboxModel({ + this.customerId, + this.customerName, + this.customerProfileImage, + this.lastMessage, + this.orderId, + this.restaurantId, + this.restaurantName, + this.restaurantProfileImage, + this.lastSenderId, + this.chatType, + this.createdAt, + }); + + factory InboxModel.fromJson(Map parsedJson) { + return InboxModel( + customerId: parsedJson['customerId'] ?? '', + customerName: parsedJson['customerName'] ?? '', + customerProfileImage: parsedJson['customerProfileImage'] ?? '', + lastMessage: parsedJson['lastMessage'], + orderId: parsedJson['orderId'], + restaurantId: parsedJson['restaurantId'] ?? '', + restaurantName: parsedJson['restaurantName'] ?? '', + lastSenderId: parsedJson['lastSenderId'] ?? '', + chatType: parsedJson['chatType'] ?? '', + restaurantProfileImage: parsedJson['restaurantProfileImage'] ?? '', + createdAt: parsedJson['createdAt'] ?? Timestamp.now(), + ); + } + + Map toJson() { + return { + 'customerId': customerId, + 'customerName': customerName, + 'customerProfileImage': customerProfileImage, + 'lastMessage': lastMessage, + 'orderId': orderId, + 'restaurantId': restaurantId, + 'restaurantName': restaurantName, + 'restaurantProfileImage': restaurantProfileImage, + 'lastSenderId': lastSenderId, + 'chatType': chatType, + 'createdAt': createdAt, + }; + } +} diff --git a/lib/models/language_model.dart b/lib/models/language_model.dart new file mode 100644 index 0000000..43a8efc --- /dev/null +++ b/lib/models/language_model.dart @@ -0,0 +1,27 @@ +class LanguageModel { + bool? isActive; + String? slug; + String? title; + String? image; + bool? isRtl; + + LanguageModel({this.isActive, this.slug, this.title, this.isRtl, this.image}); + + LanguageModel.fromJson(Map json) { + isActive = json['isActive']; + slug = json['slug']; + title = json['title']; + isRtl = json['is_rtl']; + image = json['image']; + } + + Map toJson() { + final Map data = {}; + data['isActive'] = isActive; + data['slug'] = slug; + data['title'] = title; + data['is_rtl'] = isRtl; + data['image'] = image; + return data; + } +} diff --git a/lib/models/mail_setting.dart b/lib/models/mail_setting.dart new file mode 100644 index 0000000..a61afaf --- /dev/null +++ b/lib/models/mail_setting.dart @@ -0,0 +1,44 @@ +class MailSettings { + String? emailSetting; + String? fromName; + String? host; + String? mailEncryptionType; + String? mailMethod; + String? password; + String? port; + String? userName; + + MailSettings( + {this.emailSetting, + this.fromName, + this.host, + this.mailEncryptionType, + this.mailMethod, + this.password, + this.port, + this.userName}); + + MailSettings.fromJson(Map json) { + emailSetting = json['emailSetting']; + fromName = json['fromName']; + host = json['host']; + mailEncryptionType = json['mailEncryptionType']; + mailMethod = json['mailMethod']; + password = json['password']; + port = json['port']; + userName = json['userName']; + } + + Map toJson() { + final Map data = {}; + data['emailSetting'] = emailSetting; + data['fromName'] = fromName; + data['host'] = host; + data['mailEncryptionType'] = mailEncryptionType; + data['mailMethod'] = mailMethod; + data['password'] = password; + data['port'] = port; + data['userName'] = userName; + return data; + } +} diff --git a/lib/models/notification_model.dart b/lib/models/notification_model.dart new file mode 100644 index 0000000..1886425 --- /dev/null +++ b/lib/models/notification_model.dart @@ -0,0 +1,26 @@ + +class NotificationModel { + String? subject; + String? id; + String? type; + String? message; + + NotificationModel( + { this.subject, this.id, this.type, this.message}); + + NotificationModel.fromJson(Map json) { + subject = json['subject']; + id = json['id']; + type = json['type']; + message = json['message']; + } + + Map toJson() { + final Map data = {}; + data['subject'] = subject; + data['id'] = id; + data['type'] = type; + data['message'] = message; + return data; + } +} diff --git a/lib/models/on_boarding_model.dart b/lib/models/on_boarding_model.dart new file mode 100644 index 0000000..336899e --- /dev/null +++ b/lib/models/on_boarding_model.dart @@ -0,0 +1,24 @@ +class OnBoardingModel { + String? description; + String? id; + String? title; + String? image; + + OnBoardingModel({this.description, this.id, this.title, this.image}); + + OnBoardingModel.fromJson(Map json) { + description = json['description']; + id = json['id']; + title = json['title']; + image = json['image']; + } + + Map toJson() { + final Map data = {}; + data['description'] = description; + data['id'] = id; + data['title'] = title; + data['image'] = image; + return data; + } +} diff --git a/lib/models/onprovider_order_model.dart b/lib/models/onprovider_order_model.dart new file mode 100644 index 0000000..376bccd --- /dev/null +++ b/lib/models/onprovider_order_model.dart @@ -0,0 +1,145 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/provider_serivce_model.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; + +class OnProviderOrderModel { + String authorID, payment_method; + UserModel author; + Timestamp createdAt; + String? sectionId; + ProviderServiceModel provider; + String status; + ShippingAddress? address; + String id; + List? taxModel; + Timestamp? scheduleDateTime; + Timestamp? newScheduleDateTime; + Timestamp? startTime; + Timestamp? endTime; + String? notes; + String? discount; + String? discountType; + String? discountLabel; + String? couponCode; + double quantity; + String? reason; + String? otp; + String? adminCommission; + String? adminCommissionType; + String? extraCharges; + String? extraChargesDescription; + bool? paymentStatus; + bool? extraPaymentStatus; + String? workerId; + + OnProviderOrderModel({ + this.sectionId = '', + this.authorID = '', + this.payment_method = '', + author, + createdAt, + provider, + this.status = '', + this.address, + this.id = '', + this.taxModel, + scheduleDateTime, + this.newScheduleDateTime, + this.startTime, + this.endTime, + this.notes = '', + this.discount, + this.discountType, + this.discountLabel, + this.couponCode, + this.quantity = 0.0, + this.reason, + this.otp, + this.adminCommission, + this.adminCommissionType, + this.extraCharges = '', + this.extraChargesDescription = '', + this.paymentStatus, + this.extraPaymentStatus, + this.workerId, + }) : author = author ?? UserModel(), + createdAt = createdAt ?? Timestamp.now(), + provider = provider ?? ProviderServiceModel(), + scheduleDateTime = scheduleDateTime ?? Timestamp.now(); + + factory OnProviderOrderModel.fromJson(Map parsedJson) { + List? taxList; + if (parsedJson['taxSetting'] != null) { + taxList = []; + parsedJson['taxSetting'].forEach((v) { + taxList!.add(TaxModel.fromJson(v)); + }); + } + return OnProviderOrderModel( + author: parsedJson.containsKey('author') ? UserModel.fromJson(parsedJson['author']) : UserModel(), + authorID: parsedJson['authorID'] ?? '', + address: parsedJson.containsKey('address') ? ShippingAddress.fromJson(parsedJson['address']) : ShippingAddress(), + createdAt: parsedJson['createdAt'] ?? Timestamp.now(), + id: parsedJson['id'] ?? '', + payment_method: parsedJson['payment_method'] ?? '', + taxModel: taxList, + sectionId: parsedJson['sectionId'] ?? '', + status: parsedJson['status'] ?? '', + provider: parsedJson.containsKey('provider') ? ProviderServiceModel.fromJson(parsedJson['provider']) : ProviderServiceModel(), + notes: parsedJson['notes'] ?? "", + scheduleDateTime: parsedJson['scheduleDateTime'] ?? Timestamp.now(), + newScheduleDateTime: parsedJson['newScheduleDateTime'], + startTime: parsedJson['startTime'], + endTime: parsedJson['endTime'], + discount: parsedJson['discount'] ?? "0.0", + discountLabel: parsedJson['discountLabel'] ?? "0.0", + discountType: parsedJson['discountType'] ?? "", + couponCode: parsedJson['couponCode'] ?? "", + quantity: double.parse("${parsedJson['quantity'] ?? 0.0}"), + reason: parsedJson['reason'] ?? '', + otp: parsedJson['otp'] ?? '', + adminCommission: parsedJson['adminCommission'] ?? "", + adminCommissionType: parsedJson['adminCommissionType'] ?? "", + extraCharges: parsedJson["extraCharges"] ?? "0.0", + paymentStatus: parsedJson['paymentStatus'], + extraPaymentStatus: parsedJson['extraPaymentStatus'], + workerId: parsedJson['workerId'] ?? "", + extraChargesDescription: parsedJson['extraChargesDescription'] ?? "", + ); + } + + Map toJson() { + return { + 'address': address?.toJson(), + 'author': author.toJson(), + 'authorID': authorID, + 'payment_method': payment_method, + 'createdAt': createdAt, + 'id': id, + 'status': status, + 'provider': provider.toJson(), + 'sectionId': sectionId, + "taxSetting": taxModel?.map((v) => v.toJson()).toList(), + "scheduleDateTime": scheduleDateTime, + "newScheduleDateTime": newScheduleDateTime, + "startTime": startTime, + "endTime": endTime, + "notes": notes, + 'discount': discount, + "discountLabel": discountLabel, + "discountType": discountType, + "couponCode": couponCode, + 'quantity': quantity, + 'reason': reason, + 'otp': otp, + "adminCommission": adminCommission, + "adminCommissionType": adminCommissionType, + 'extraCharges': extraCharges, + 'paymentStatus': paymentStatus, + 'extraPaymentStatus': extraPaymentStatus, + 'workerId': workerId, + 'extraChargesDescription': extraChargesDescription, + }; + } +} diff --git a/lib/models/order_model.dart b/lib/models/order_model.dart new file mode 100644 index 0000000..6da5c45 --- /dev/null +++ b/lib/models/order_model.dart @@ -0,0 +1,180 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; + +import 'cart_product_model.dart'; +import 'cashback_model.dart'; + +class OrderModel { + ShippingAddress? address; + String? status; + String? couponId; + String? vendorID; + String? driverID; + num? discount; + String? authorID; + String? estimatedTimeToPrepare; + Timestamp? createdAt; + Timestamp? triggerDelivery; + List? taxSetting; + String? paymentMethod; + List? products; + String? adminCommissionType; + VendorModel? vendor; + String? id; + String? adminCommission; + String? couponCode; + String? sectionId; + Map? specialDiscount; + String? deliveryCharge; + Timestamp? scheduleTime; + String? tipAmount; + String? notes; + UserModel? author; + UserModel? driver; + bool? takeAway; + List? rejectedByDrivers; + CashbackModel? cashback; + String? courierCompanyName; + String? courierTrackingId; + + OrderModel({ + this.address, + this.status, + this.couponId, + this.vendorID, + this.driverID, + this.discount, + this.authorID, + this.estimatedTimeToPrepare, + this.createdAt, + this.triggerDelivery, + this.taxSetting, + this.paymentMethod, + this.products, + this.adminCommissionType, + this.vendor, + this.id, + this.adminCommission, + this.couponCode, + this.sectionId, + this.specialDiscount, + this.deliveryCharge, + this.scheduleTime, + this.tipAmount, + this.notes, + this.author, + this.driver, + this.takeAway, + this.rejectedByDrivers, + this.cashback, + this.courierCompanyName, + this.courierTrackingId, + }); + + OrderModel.fromJson(Map json) { + address = + json['address'] != null + ? ShippingAddress.fromJson(json['address']) + : null; + status = json['status']; + couponId = json['couponId']; + vendorID = json['vendorID']; + driverID = json['driverID']; + discount = json['discount']; + authorID = json['authorID']; + estimatedTimeToPrepare = json['estimatedTimeToPrepare']; + createdAt = json['createdAt']; + courierCompanyName = json['courierCompanyName']; + courierTrackingId = json['courierTrackingId']; + triggerDelivery = json['triggerDelevery'] ?? Timestamp.now(); + if (json['taxSetting'] != null) { + taxSetting = []; + json['taxSetting'].forEach((v) { + taxSetting!.add(TaxModel.fromJson(v)); + }); + } + paymentMethod = json['payment_method']; + if (json['products'] != null) { + products = []; + json['products'].forEach((v) { + products!.add(CartProductModel.fromJson(v)); + }); + } + adminCommissionType = json['adminCommissionType']; + vendor = + json['vendor'] != null ? VendorModel.fromJson(json['vendor']) : null; + id = json['id']; + adminCommission = json['adminCommission']; + couponCode = json['couponCode']; + sectionId = json['section_id']; + specialDiscount = json['specialDiscount']; + deliveryCharge = + json['deliveryCharge'].toString().isEmpty + ? "0.0" + : json['deliveryCharge'] ?? '0.0'; + scheduleTime = json['scheduleTime']; + tipAmount = + json['tip_amount'].toString().isEmpty + ? "0.0" + : json['tip_amount'] ?? "0.0"; + notes = json['notes']; + author = json['author'] != null ? UserModel.fromJson(json['author']) : null; + driver = json['driver'] != null ? UserModel.fromJson(json['driver']) : null; + takeAway = json['takeAway']; + rejectedByDrivers = json['rejectedByDrivers'] ?? []; + cashback = + json['cashback'] != null + ? CashbackModel.fromJson(json['cashback']) + : null; + } + + Map toJson() { + final Map data = {}; + if (address != null) { + data['address'] = address!.toJson(); + } + data['status'] = status; + data['couponId'] = couponId; + data['vendorID'] = vendorID; + data['driverID'] = driverID; + data['discount'] = discount; + data['authorID'] = authorID; + data['estimatedTimeToPrepare'] = estimatedTimeToPrepare; + data['createdAt'] = createdAt; + data['triggerDelivery'] = triggerDelivery; + if (taxSetting != null) { + data['taxSetting'] = taxSetting!.map((v) => v.toJson()).toList(); + } + data['payment_method'] = paymentMethod; + if (products != null) { + data['products'] = products!.map((v) => v.toJson()).toList(); + } + data['adminCommissionType'] = adminCommissionType; + if (vendor != null) { + data['vendor'] = vendor!.toJson(); + } + data['id'] = id; + data['adminCommission'] = adminCommission; + data['couponCode'] = couponCode; + data['section_id'] = sectionId; + data['specialDiscount'] = specialDiscount; + data['deliveryCharge'] = deliveryCharge; + data['scheduleTime'] = scheduleTime; + data['tip_amount'] = tipAmount; + data['courierCompanyName'] = courierCompanyName; + data['courierTrackingId'] = courierTrackingId; + data['notes'] = notes; + if (author != null) { + data['author'] = author!.toJson(); + } + if (driver != null) { + data['driver'] = driver!.toJson(); + } + data['takeAway'] = takeAway; + data['rejectedByDrivers'] = rejectedByDrivers; + data['cashback'] = cashback?.toJson(); + return data; + } +} diff --git a/lib/models/parcel_category.dart b/lib/models/parcel_category.dart new file mode 100644 index 0000000..0651d85 --- /dev/null +++ b/lib/models/parcel_category.dart @@ -0,0 +1,27 @@ +class ParcelCategory { + String? image; + int? setOrder; + bool? publish; + String? id; + String? title; + + ParcelCategory({this.image, this.setOrder, this.publish, this.id, this.title}); + + ParcelCategory.fromJson(Map json) { + image = json['image']; + setOrder = json['set_order']; + publish = json['publish']; + id = json['id']; + title = json['title']; + } + + Map toJson() { + final Map data = {}; + data['image'] = image; + data['set_order'] = setOrder; + data['publish'] = publish; + data['id'] = id; + data['title'] = title; + return data; + } +} diff --git a/lib/models/parcel_order_model.dart b/lib/models/parcel_order_model.dart new file mode 100644 index 0000000..6fac3b3 --- /dev/null +++ b/lib/models/parcel_order_model.dart @@ -0,0 +1,238 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; + +class ParcelOrderModel { + UserModel? author; + UserModel? driver; + + LocationInformation? sender; + Timestamp? senderPickupDateTime; + String? id; + String? driverId; + + UserLocation? receiverLatLong; + bool? paymentCollectByReceiver; + List? taxSetting; + String? adminCommissionType; + List? rejectedByDrivers; + String? adminCommission; + List? parcelImages; + String? parcelWeight; + String? discountType; + String? discountLabel; + LocationInformation? receiver; + String? paymentMethod; + String? distance; + Timestamp? createdAt; + bool? isSchedule; + String? subTotal; + Timestamp? triggerDelevery; + String? status; + String? parcelType; + bool? sendToDriver; + String? sectionId; + UserLocation? senderLatLong; + String? authorID; + String? parcelWeightCharge; + String? parcelCategoryID; + String? discount; + Timestamp? receiverPickupDateTime; + String? note; + String? receiverNote; + String? senderZoneId; + String? receiverZoneId; + G? sourcePoint; + G? destinationPoint; + List? statusHistory; + + + ParcelOrderModel({ + this.author, + this.sender, + this.senderPickupDateTime, + this.id, + this.driverId, + this.receiverLatLong, + this.paymentCollectByReceiver, + this.taxSetting, + this.adminCommissionType, + this.rejectedByDrivers, + this.adminCommission, + this.parcelImages, + this.parcelWeight, + this.discountType, + this.discountLabel, + this.receiver, + this.paymentMethod, + this.distance, + this.createdAt, + this.isSchedule, + this.subTotal, + this.triggerDelevery, + this.status, + this.parcelType, + this.sendToDriver, + this.sectionId, + this.senderLatLong, + this.authorID, + this.parcelWeightCharge, + this.parcelCategoryID, + this.discount, + this.receiverPickupDateTime, + this.note, + this.receiverNote, + this.senderZoneId, + this.sourcePoint, + this.destinationPoint, + this.receiverZoneId, + this.driver, + this.statusHistory, + }); + + ParcelOrderModel.fromJson(Map json) { + author = json['author'] != null ? UserModel.fromJson(json['author']) : null; + driver = json['driver'] != null ? UserModel.fromJson(json['driver']) : null; + sender = json['sender'] != null ? LocationInformation.fromJson(json['sender']) : null; + senderPickupDateTime = json['senderPickupDateTime']; + id = json['id']; + driverId = json['driverId']; + receiverLatLong = json['receiverLatLong'] != null ? UserLocation.fromJson(json['receiverLatLong']) : null; + paymentCollectByReceiver = json['paymentCollectByReceiver']; + if (json['taxSetting'] != null) { + taxSetting = []; + json['taxSetting'].forEach((v) { + taxSetting!.add(TaxModel.fromJson(v)); + }); + } + adminCommissionType = json['adminCommissionType']; + rejectedByDrivers = json['rejectedByDrivers'] ?? []; + adminCommission = json['adminCommission']; + parcelImages = json['parcelImages'] ?? []; + parcelWeight = json['parcelWeight']; + discountType = json['discountType']; + discountLabel = json['discountLabel']; + receiver = json['receiver'] != null ? LocationInformation.fromJson(json['receiver']) : null; + paymentMethod = json['payment_method']; + distance = json['distance']; + createdAt = json['createdAt']; + isSchedule = json['isSchedule']; + subTotal = json['subTotal']; + triggerDelevery = json['trigger_delevery']; + status = json['status']; + parcelType = json['parcelType']; + sendToDriver = json['sendToDriver']; + sectionId = json['sectionId']; + senderLatLong = json['senderLatLong'] != null ? UserLocation.fromJson(json['senderLatLong']) : null; + authorID = json['authorID']; + parcelWeightCharge = json['parcelWeightCharge']; + parcelCategoryID = json['parcelCategoryID']; + discount = json['discount']; + receiverPickupDateTime = json['receiverPickupDateTime']; + note = json['note']; + senderZoneId = json['senderZoneId']; + receiverZoneId = json['receiverZoneId']; + receiverNote = json['receiverNote']; + sourcePoint = json['sourcePoint'] != null ? G.fromJson(json['sourcePoint']) : null; + destinationPoint = json['destinationPoint'] != null ? G.fromJson(json['destinationPoint']) : null; + } + + Map toJson() { + final Map data = {}; + if (author != null) { + data['author'] = author!.toJson(); + } if (driver != null) { + data['driver'] = driver!.toJson(); + } + if (sender != null) { + data['sender'] = sender!.toJson(); + } + data['senderPickupDateTime'] = senderPickupDateTime; + data['id'] = id; + if (receiverLatLong != null) { + data['receiverLatLong'] = receiverLatLong!.toJson(); + } + data['paymentCollectByReceiver'] = paymentCollectByReceiver; + if (taxSetting != null) { + data['taxSetting'] = taxSetting!.map((v) => v.toJson()).toList(); + } + data['driverId'] = driverId; + data['adminCommissionType'] = adminCommissionType; + data['rejectedByDrivers'] = rejectedByDrivers; + data['adminCommission'] = adminCommission; + data['parcelImages'] = parcelImages; + data['parcelWeight'] = parcelWeight; + data['discountType'] = discountType; + data['discountLabel'] = discountLabel; + if (receiver != null) { + data['receiver'] = receiver!.toJson(); + } + data['payment_method'] = paymentMethod; + data['distance'] = distance; + data['createdAt'] = createdAt; + data['isSchedule'] = isSchedule; + data['subTotal'] = subTotal; + data['trigger_delevery'] = triggerDelevery; + data['status'] = status; + data['parcelType'] = parcelType; + data['sendToDriver'] = sendToDriver; + data['sectionId'] = sectionId; + if (senderLatLong != null) { + data['senderLatLong'] = senderLatLong!.toJson(); + } + if (sourcePoint != null) { + data['sourcePoint'] = sourcePoint!.toJson(); + } + if (destinationPoint != null) { + data['destinationPoint'] = destinationPoint!.toJson(); + } + data['authorID'] = authorID; + data['parcelWeightCharge'] = parcelWeightCharge; + data['parcelCategoryID'] = parcelCategoryID; + data['discount'] = discount; + data['receiverPickupDateTime'] = receiverPickupDateTime; + data['note'] = note; + data['senderZoneId'] = senderZoneId; + data['receiverZoneId'] = receiverZoneId; + data['receiverNote'] = receiverNote; + return data; + } +} + +class LocationInformation { + String? address; + String? name; + String? phone; + + LocationInformation({this.address, this.name, this.phone}); + + LocationInformation.fromJson(Map json) { + address = json['address']; + name = json['name']; + phone = json['phone']; + } + + Map toJson() { + final Map data = {}; + data['address'] = address; + data['name'] = name; + data['phone'] = phone; + return data; + } +} + +class ParcelStatus { + final String? status; + final DateTime? time; + + ParcelStatus({this.status, this.time}); + + factory ParcelStatus.fromMap(Map map) { + return ParcelStatus(status: map['status'] as String?, time: map['time'] != null ? (map['time'] as Timestamp).toDate() : null); + } + + Map toMap() { + return {'status': status, 'time': time != null ? Timestamp.fromDate(time!) : null}; + } +} diff --git a/lib/models/parcel_weight_model.dart b/lib/models/parcel_weight_model.dart new file mode 100644 index 0000000..a0339c5 --- /dev/null +++ b/lib/models/parcel_weight_model.dart @@ -0,0 +1,21 @@ +class ParcelWeightModel { + String? deliveryCharge; + String? id; + String? title; + + ParcelWeightModel({this.deliveryCharge, this.id, this.title}); + + ParcelWeightModel.fromJson(Map json) { + deliveryCharge = json['delivery_charge']; + id = json['id']; + title = json['title']; + } + + Map toJson() { + final Map data = {}; + data['delivery_charge'] = deliveryCharge; + data['id'] = id; + data['title'] = title; + return data; + } +} diff --git a/lib/models/payment_model/cod_setting_model.dart b/lib/models/payment_model/cod_setting_model.dart new file mode 100644 index 0000000..16b3978 --- /dev/null +++ b/lib/models/payment_model/cod_setting_model.dart @@ -0,0 +1,15 @@ +class CodSettingModel { + bool? isEnabled; + + CodSettingModel({this.isEnabled}); + + CodSettingModel.fromJson(Map json) { + isEnabled = json['isEnabled']; + } + + Map toJson() { + final Map data = {}; + data['isEnabled'] = isEnabled; + return data; + } +} diff --git a/lib/models/payment_model/flutter_wave_model.dart b/lib/models/payment_model/flutter_wave_model.dart new file mode 100644 index 0000000..8931fd1 --- /dev/null +++ b/lib/models/payment_model/flutter_wave_model.dart @@ -0,0 +1,30 @@ +class FlutterWaveModel { + bool? isSandbox; + bool? isWithdrawEnabled; + String? publicKey; + String? encryptionKey; + bool? isEnable; + String? secretKey; + + FlutterWaveModel({this.isSandbox, this.isWithdrawEnabled, this.publicKey, this.encryptionKey, this.isEnable, this.secretKey}); + + FlutterWaveModel.fromJson(Map json) { + isSandbox = json['isSandbox']; + isWithdrawEnabled = json['isWithdrawEnabled']; + publicKey = json['publicKey']; + encryptionKey = json['encryptionKey']; + isEnable = json['isEnable']; + secretKey = json['secretKey']; + } + + Map toJson() { + final Map data = {}; + data['isSandbox'] = isSandbox; + data['isWithdrawEnabled'] = isWithdrawEnabled; + data['publicKey'] = publicKey; + data['encryptionKey'] = encryptionKey; + data['isEnable'] = isEnable; + data['secretKey'] = secretKey; + return data; + } +} diff --git a/lib/models/payment_model/mercado_pago_model.dart b/lib/models/payment_model/mercado_pago_model.dart new file mode 100644 index 0000000..3d15f60 --- /dev/null +++ b/lib/models/payment_model/mercado_pago_model.dart @@ -0,0 +1,24 @@ +class MercadoPagoModel { + bool? isSandboxEnabled; + bool? isEnabled; + String? accessToken; + String? publicKey; + + MercadoPagoModel({this.isSandboxEnabled, this.isEnabled, this.accessToken, this.publicKey}); + + MercadoPagoModel.fromJson(Map json) { + isSandboxEnabled = json['isSandboxEnabled']; + isEnabled = json['isEnabled']; + accessToken = json['AccessToken']; + publicKey = json['PublicKey']; + } + + Map toJson() { + final Map data = {}; + data['isSandboxEnabled'] = isSandboxEnabled; + data['isEnabled'] = isEnabled; + data['AccessToken'] = accessToken; + data['PublicKey'] = publicKey; + return data; + } +} diff --git a/lib/models/payment_model/mid_trans.dart b/lib/models/payment_model/mid_trans.dart new file mode 100644 index 0000000..31107db --- /dev/null +++ b/lib/models/payment_model/mid_trans.dart @@ -0,0 +1,34 @@ +class MidTrans { + bool? enable; + String? name; + bool? isSandbox; + String? serverKey; + String? image; + + MidTrans({ + this.name, + this.enable, + this.serverKey, + this.isSandbox, + this.image, + }); + + MidTrans.fromJson(Map json) { + enable = json['enable']; + name = json['name']; + isSandbox = json['isSandbox']; + serverKey = json['serverKey']; + image = json['image']; + } + + Map toJson() { + final Map data = {}; + + data['enable'] = enable; + data['name'] = name; + data['isSandbox'] = isSandbox; + data['serverKey'] = serverKey; + data['image'] = image; + return data; + } +} diff --git a/lib/models/payment_model/orange_money.dart b/lib/models/payment_model/orange_money.dart new file mode 100644 index 0000000..4aee198 --- /dev/null +++ b/lib/models/payment_model/orange_money.dart @@ -0,0 +1,60 @@ +class OrangeMoney { + String? image; + String? clientId; + String? auth; + bool? enable; + String? name; + String? notifyUrl; + String? clientSecret; + bool? isSandbox; + String? returnUrl; + String? merchantKey; + String? cancelUrl; + String? notifUrl; + + OrangeMoney( + {this.image, + this.clientId, + this.auth, + this.enable, + this.name, + this.notifyUrl, + this.clientSecret, + this.isSandbox, + this.returnUrl, + this.cancelUrl, + this.notifUrl, + this.merchantKey}); + + OrangeMoney.fromJson(Map json) { + image = json['image']; + clientId = json['clientId']; + auth = json['auth']; + enable = json['enable']; + name = json['name']; + notifyUrl = json['notifyUrl']; + clientSecret = json['clientSecret']; + isSandbox = json['isSandbox']; + returnUrl = json['returnUrl']; + merchantKey = json['merchantKey']; + cancelUrl = json['cancelUrl']; + notifUrl = json['notifUrl']; + } + + Map toJson() { + final Map data = {}; + data['image'] = image; + data['clientId'] = clientId; + data['auth'] = auth; + data['enable'] = enable; + data['name'] = name; + data['notifyUrl'] = notifyUrl; + data['clientSecret'] = clientSecret; + data['isSandbox'] = isSandbox; + data['returnUrl'] = returnUrl; + data['merchantKey'] = merchantKey; + data['cancelUrl'] = cancelUrl; + data['notifUrl'] = notifUrl; + return data; + } +} diff --git a/lib/models/payment_model/pay_fast_model.dart b/lib/models/payment_model/pay_fast_model.dart new file mode 100644 index 0000000..449e222 --- /dev/null +++ b/lib/models/payment_model/pay_fast_model.dart @@ -0,0 +1,33 @@ +class PayFastModel { + String? returnUrl; + String? cancelUrl; + String? notifyUrl; + String? merchantKey; + bool? isEnable; + String? merchantId; + bool? isSandbox; + + PayFastModel({this.returnUrl, this.cancelUrl, this.notifyUrl, this.merchantKey, this.isEnable, this.merchantId, this.isSandbox}); + + PayFastModel.fromJson(Map json) { + returnUrl = json['return_url']; + cancelUrl = json['cancel_url']; + notifyUrl = json['notify_url']; + merchantKey = json['merchant_key']; + isEnable = json['isEnable']; + merchantId = json['merchant_id']; + isSandbox = json['isSandbox']; + } + + Map toJson() { + final Map data = {}; + data['return_url'] = returnUrl; + data['cancel_url'] = cancelUrl; + data['notify_url'] = notifyUrl; + data['merchant_key'] = merchantKey; + data['isEnable'] = isEnable; + data['merchant_id'] = merchantId; + data['isSandbox'] = isSandbox; + return data; + } +} diff --git a/lib/models/payment_model/pay_stack_model.dart b/lib/models/payment_model/pay_stack_model.dart new file mode 100644 index 0000000..1255db5 --- /dev/null +++ b/lib/models/payment_model/pay_stack_model.dart @@ -0,0 +1,30 @@ +class PayStackModel { + bool? isSandbox; + String? callbackURL; + String? publicKey; + String? secretKey; + bool? isEnable; + String? webhookURL; + + PayStackModel({this.isSandbox, this.callbackURL, this.publicKey, this.secretKey, this.isEnable, this.webhookURL}); + + PayStackModel.fromJson(Map json) { + isSandbox = json['isSandbox']; + callbackURL = json['callbackURL']; + publicKey = json['publicKey']; + secretKey = json['secretKey']; + isEnable = json['isEnable']; + webhookURL = json['webhookURL']; + } + + Map toJson() { + final Map data = {}; + data['isSandbox'] = isSandbox; + data['callbackURL'] = callbackURL; + data['publicKey'] = publicKey; + data['secretKey'] = secretKey; + data['isEnable'] = isEnable; + data['webhookURL'] = webhookURL; + return data; + } +} diff --git a/lib/models/payment_model/paypal_model.dart b/lib/models/payment_model/paypal_model.dart new file mode 100644 index 0000000..f495415 --- /dev/null +++ b/lib/models/payment_model/paypal_model.dart @@ -0,0 +1,30 @@ +class PayPalModel { + String? paypalSecret; + bool? isWithdrawEnabled; + String? paypalAppId; + bool? isEnabled; + bool? isLive; + String? paypalClient; + + PayPalModel({this.paypalSecret, this.isWithdrawEnabled, this.paypalAppId, this.isEnabled, this.isLive, this.paypalClient}); + + PayPalModel.fromJson(Map json) { + paypalSecret = json['paypalSecret']; + isWithdrawEnabled = json['isWithdrawEnabled']; + paypalAppId = json['paypalAppId']; + isEnabled = json['isEnabled']; + isLive = json['isLive']; + paypalClient = json['paypalClient']; + } + + Map toJson() { + final Map data = {}; + data['paypalSecret'] = paypalSecret; + data['isWithdrawEnabled'] = isWithdrawEnabled; + data['paypalAppId'] = paypalAppId; + data['isEnabled'] = isEnabled; + data['isLive'] = isLive; + data['paypalClient'] = paypalClient; + return data; + } +} diff --git a/lib/models/payment_model/paytm_model.dart b/lib/models/payment_model/paytm_model.dart new file mode 100644 index 0000000..fe7646b --- /dev/null +++ b/lib/models/payment_model/paytm_model.dart @@ -0,0 +1,24 @@ +class PaytmModel { + String? paytmMID; + String? pAYTMMERCHANTKEY; + bool? isEnabled; + bool? isSandboxEnabled; + + PaytmModel({this.paytmMID, this.pAYTMMERCHANTKEY, this.isEnabled, this.isSandboxEnabled}); + + PaytmModel.fromJson(Map json) { + paytmMID = json['PaytmMID']; + pAYTMMERCHANTKEY = json['PAYTM_MERCHANT_KEY']; + isEnabled = json['isEnabled']; + isSandboxEnabled = json['isSandboxEnabled']; + } + + Map toJson() { + final Map data = {}; + data['PaytmMID'] = paytmMID; + data['PAYTM_MERCHANT_KEY'] = pAYTMMERCHANTKEY; + data['isEnabled'] = isEnabled; + data['isSandboxEnabled'] = isSandboxEnabled; + return data; + } +} diff --git a/lib/models/payment_model/razorpay_model.dart b/lib/models/payment_model/razorpay_model.dart new file mode 100644 index 0000000..6aaadc8 --- /dev/null +++ b/lib/models/payment_model/razorpay_model.dart @@ -0,0 +1,27 @@ +class RazorPayModel { + String? razorpaySecret; + bool? isWithdrawEnabled; + bool? isSandboxEnabled; + bool? isEnabled; + String? razorpayKey; + + RazorPayModel({this.razorpaySecret, this.isWithdrawEnabled, this.isSandboxEnabled, this.isEnabled, this.razorpayKey}); + + RazorPayModel.fromJson(Map json) { + razorpaySecret = json['razorpaySecret']; + isWithdrawEnabled = json['isWithdrawEnabled']; + isSandboxEnabled = json['isSandboxEnabled']; + isEnabled = json['isEnabled']; + razorpayKey = json['razorpayKey']; + } + + Map toJson() { + final Map data = {}; + data['razorpaySecret'] = razorpaySecret; + data['isWithdrawEnabled'] = isWithdrawEnabled; + data['isSandboxEnabled'] = isSandboxEnabled; + data['isEnabled'] = isEnabled; + data['razorpayKey'] = razorpayKey; + return data; + } +} diff --git a/lib/models/payment_model/stripe_model.dart b/lib/models/payment_model/stripe_model.dart new file mode 100644 index 0000000..94981f3 --- /dev/null +++ b/lib/models/payment_model/stripe_model.dart @@ -0,0 +1,30 @@ +class StripeModel { + String? stripeSecret; + String? clientpublishableKey; + bool? isWithdrawEnabled; + bool? isEnabled; + bool? isSandboxEnabled; + String? stripeKey; + + StripeModel({this.stripeSecret, this.clientpublishableKey, this.isWithdrawEnabled, this.isEnabled, this.isSandboxEnabled, this.stripeKey}); + + StripeModel.fromJson(Map json) { + stripeSecret = json['stripeSecret']; + clientpublishableKey = json['clientpublishableKey']; + isWithdrawEnabled = json['isWithdrawEnabled']; + isEnabled = json['isEnabled']; + isSandboxEnabled = json['isSandboxEnabled']; + stripeKey = json['stripeKey']; + } + + Map toJson() { + final Map data = {}; + data['stripeSecret'] = stripeSecret; + data['clientpublishableKey'] = clientpublishableKey; + data['isWithdrawEnabled'] = isWithdrawEnabled; + data['isEnabled'] = isEnabled; + data['isSandboxEnabled'] = isSandboxEnabled; + data['stripeKey'] = stripeKey; + return data; + } +} diff --git a/lib/models/payment_model/wallet_setting_model.dart b/lib/models/payment_model/wallet_setting_model.dart new file mode 100644 index 0000000..fff6fc0 --- /dev/null +++ b/lib/models/payment_model/wallet_setting_model.dart @@ -0,0 +1,15 @@ +class WalletSettingModel { + bool? isEnabled; + + WalletSettingModel({this.isEnabled}); + + WalletSettingModel.fromJson(Map json) { + isEnabled = json['isEnabled']; + } + + Map toJson() { + final Map data = {}; + data['isEnabled'] = isEnabled; + return data; + } +} diff --git a/lib/models/payment_model/xendit.dart b/lib/models/payment_model/xendit.dart new file mode 100644 index 0000000..2594114 --- /dev/null +++ b/lib/models/payment_model/xendit.dart @@ -0,0 +1,34 @@ +class Xendit { + bool? enable; + String? name; + bool? isSandbox; + String? apiKey; + String? image; + + Xendit({ + this.name, + this.enable, + this.apiKey, + this.isSandbox, + this.image, + }); + + Xendit.fromJson(Map json) { + enable = json['enable']; + name = json['name']; + isSandbox = json['isSandbox']; + apiKey = json['apiKey']; + image = json['image']; + } + + Map toJson() { + final Map data = {}; + + data['enable'] = enable; + data['name'] = name; + data['isSandbox'] = isSandbox; + data['apiKey'] = apiKey; + data['image'] = image; + return data; + } +} diff --git a/lib/models/popular_destination.dart b/lib/models/popular_destination.dart new file mode 100644 index 0000000..81fd072 --- /dev/null +++ b/lib/models/popular_destination.dart @@ -0,0 +1,28 @@ +class PopularDestination { + String? image; + String? id; + String? title; + double? latitude; + double? longitude; + + PopularDestination( + {this.image, this.id, this.title, this.latitude, this.longitude}); + + PopularDestination.fromJson(Map json) { + image = json['image']; + id = json['id']; + title = json['title']; + latitude = json['latitude']; + longitude = json['longitude']; + } + + Map toJson() { + final Map data = {}; + data['image'] = image; + data['id'] = id; + data['title'] = title; + data['latitude'] = latitude; + data['longitude'] = longitude; + return data; + } +} diff --git a/lib/models/product_model.dart b/lib/models/product_model.dart new file mode 100644 index 0000000..5549d1e --- /dev/null +++ b/lib/models/product_model.dart @@ -0,0 +1,243 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class ProductModel { + int? fats; + String? vendorID; + bool? veg; + bool? publish; + List? addOnsTitle; + int? calories; + int? proteins; + List? addOnsPrice; + num? reviewsSum; + bool? takeawayOption; + String? name; + Map? reviewAttributes; + Map? productSpecification; + ItemAttribute? itemAttribute; + String? id; + int? quantity; + int? grams; + num? reviewsCount; + String? disPrice; + List? photos; + bool? nonveg; + String? photo; + String? price; + String? categoryID; + String? description; + Timestamp? createdAt; + String? sectionId; + String? brandId; + bool? isDigitalProduct; + String? digitalProduct; + + ProductModel({ + this.fats, + this.vendorID, + this.veg, + this.publish, + this.addOnsTitle, + this.calories, + this.proteins, + this.addOnsPrice, + this.reviewsSum, + this.takeawayOption, + this.name, + this.reviewAttributes, + this.productSpecification, + this.itemAttribute, + this.id, + this.quantity, + this.grams, + this.reviewsCount, + this.disPrice, + this.photos, + this.nonveg, + this.photo, + this.price, + this.categoryID, + this.description, + this.createdAt, + this.sectionId, + this.brandId, + this.isDigitalProduct, + this.digitalProduct, + + }); + + ProductModel.fromJson(Map json) { + fats = json['fats']; + vendorID = json['vendorID']; + veg = json['veg']; + publish = json['publish']; + addOnsTitle = json['addOnsTitle']; + calories = json['calories']; + proteins = json['proteins']; + addOnsPrice = json['addOnsPrice']; + reviewsSum = json['reviewsSum'] ?? 0.0; + takeawayOption = json['takeawayOption']; + name = json['name']; + reviewAttributes = json['reviewAttributes']; + productSpecification = json['product_specification']; + itemAttribute = json['item_attribute'] != null ? ItemAttribute.fromJson(json['item_attribute']) : null; + id = json['id']; + quantity = json['quantity']; + grams = json['grams']; + reviewsCount = json['reviewsCount'] ?? 0.0; + disPrice = json['disPrice'] ?? "0"; + photos = json['photos'] ?? []; + nonveg = json['nonveg']; + photo = json['photo']; + price = json['price']; + categoryID = json['categoryID']; + description = json['description']; + createdAt = json['createdAt']; + sectionId = json['section_id']; + brandId = json['brandID']; + isDigitalProduct = json['isDigitalProduct']; + digitalProduct = json['digitalProduct']; + } + + Map toJson() { + final Map data = {}; + data['fats'] = fats; + data['vendorID'] = vendorID; + data['veg'] = veg; + data['publish'] = publish; + data['addOnsTitle'] = addOnsTitle; + data['addOnsPrice'] = addOnsPrice; + data['calories'] = calories; + data['proteins'] = proteins; + data['reviewsSum'] = reviewsSum; + data['takeawayOption'] = takeawayOption; + data['name'] = name; + data['reviewAttributes'] = reviewAttributes; + data['product_specification'] = productSpecification; + if (itemAttribute != null) { + data['item_attribute'] = itemAttribute!.toJson(); + } + data['id'] = id; + data['quantity'] = quantity; + data['grams'] = grams; + data['reviewsCount'] = reviewsCount; + data['disPrice'] = disPrice; + data['photos'] = photos; + data['nonveg'] = nonveg; + data['photo'] = photo; + data['price'] = price; + data['categoryID'] = categoryID; + data['description'] = description; + data['createdAt'] = createdAt; + data['section_id'] = sectionId; + data['brandID'] = brandId; + data['isDigitalProduct'] = isDigitalProduct; + data['digitalProduct'] = digitalProduct; + return data; + } +} + +class ItemAttribute { + List? attributes; + List? variants; + + ItemAttribute({this.attributes, this.variants}); + + ItemAttribute.fromJson(Map json) { + if (json['attributes'] != null) { + attributes = []; + json['attributes'].forEach((v) { + attributes!.add(Attributes.fromJson(v)); + }); + } + if (json['variants'] != null) { + variants = []; + json['variants'].forEach((v) { + variants!.add(Variants.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (attributes != null) { + data['attributes'] = attributes!.map((v) => v.toJson()).toList(); + } + if (variants != null) { + data['variants'] = variants!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Attributes { + String? attributeId; + List? attributeOptions; + + Attributes({this.attributeId, this.attributeOptions}); + + Attributes.fromJson(Map json) { + attributeId = json['attribute_id']; + attributeOptions = json['attribute_options'].cast(); + } + + Map toJson() { + final Map data = {}; + data['attribute_id'] = attributeId; + data['attribute_options'] = attributeOptions; + return data; + } +} + +class Variants { + String? variantId; + String? variantImage; + String? variantPrice; + String? variantQuantity; + String? variantSku; + + Variants({ + this.variantId, + this.variantImage, + this.variantPrice, + this.variantQuantity, + this.variantSku, + }); + + Variants.fromJson(Map json) { + variantId = json['variant_id']; + variantImage = json['variant_image']; + variantPrice = json['variant_price'] ?? '0'; + variantQuantity = json['variant_quantity'] ?? '0'; + variantSku = json['variant_sku']; + } + + Map toJson() { + final Map data = {}; + data['variant_id'] = variantId; + data['variant_image'] = variantImage; + data['variant_price'] = variantPrice; + data['variant_quantity'] = variantQuantity; + data['variant_sku'] = variantSku; + return data; + } +} + +class ReviewsAttribute { + num? reviewsCount; + num? reviewsSum; + + ReviewsAttribute({this.reviewsCount, this.reviewsSum}); + + ReviewsAttribute.fromJson(Map json) { + reviewsCount = json['reviewsCount'] ?? 0; + reviewsSum = json['reviewsSum'] ?? 0; + } + + Map toJson() { + final Map data = {}; + data['reviewsCount'] = reviewsCount; + data['reviewsSum'] = reviewsSum; + return data; + } +} diff --git a/lib/models/provider_serivce_model.dart b/lib/models/provider_serivce_model.dart new file mode 100644 index 0000000..eeb50d3 --- /dev/null +++ b/lib/models/provider_serivce_model.dart @@ -0,0 +1,190 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/subscription_plan_model.dart'; + +class ProviderServiceModel { + String? author; + String? authorName; + String? authorProfilePic; + String? sectionId; + String? subCategoryId; + String? categoryId; + Timestamp? createdAt; + String? description; + String? id; + double? latitude; + double? longitude; + List photos; + String? address; + num? reviewsCount; + num? reviewsSum; + String? title; + GeoFireData geoFireData; + String? price; + String? disPrice = "0"; + bool? publish; + String? startTime; + String? endTime; + String? priceUnit; + List days; + String? phoneNumber; + + String? subscriptionPlanId; + Timestamp? subscriptionExpiryDate; + SubscriptionPlanModel? subscriptionPlan; + String? subscriptionTotalOrders; + + ProviderServiceModel({ + this.author = '', + this.authorName = '', + this.authorProfilePic = '', + this.sectionId = '', + this.subCategoryId = '', + this.categoryId = '', + this.createdAt, + this.description = '', + this.id = '', + this.latitude = 0.1, + this.longitude = 0.1, + this.address = '', + this.reviewsCount = 0, + this.reviewsSum = 0, + this.title = '', + this.price = '', + this.disPrice, + geoFireData, + DeliveryCharge, + this.photos = const [], + this.publish = true, + this.startTime, + this.endTime, + this.priceUnit, + this.subscriptionPlanId, + this.subscriptionExpiryDate, + this.subscriptionPlan, + this.days = const [], + this.phoneNumber, + this.subscriptionTotalOrders, + }) : geoFireData = geoFireData ?? + GeoFireData( + geohash: "", + geoPoint: GeoPoint(0.0, 0.0), + ); + + factory ProviderServiceModel.fromJson(Map parsedJson) { + return ProviderServiceModel( + author: parsedJson['author'] ?? '', + authorName: parsedJson['authorName'] ?? '', + authorProfilePic: parsedJson['authorProfilePic'] ?? '', + sectionId: parsedJson['sectionId'] ?? '', + categoryId: parsedJson['categoryId'] ?? '', + subCategoryId: parsedJson['subCategoryId'] ?? '', + price: parsedJson['price'] ?? '', + disPrice: parsedJson['disPrice'] ?? '0', + createdAt: parsedJson['createdAt'] ?? Timestamp.now(), + geoFireData: parsedJson.containsKey('g') + ? GeoFireData.fromJson(parsedJson['g']) + : GeoFireData( + geohash: "", + geoPoint: GeoPoint(0.0, 0.0), + ), + description: parsedJson['description'] ?? '', + id: parsedJson['id'] ?? '', + latitude: parsedJson['latitude'] ?? 0.1, + longitude: parsedJson['longitude'] ?? 0.1, + photos: parsedJson['photos'] ?? [], + address: parsedJson['address'] ?? '', + reviewsCount: parsedJson['reviewsCount'] ?? 0, + reviewsSum: parsedJson['reviewsSum'] ?? 0, + title: parsedJson['title'] ?? '', + publish: parsedJson['publish'] ?? true, + startTime: parsedJson['startTime'], + endTime: parsedJson['endTime'], + priceUnit: parsedJson['priceUnit'], + days: parsedJson['days'] ?? [], + phoneNumber: parsedJson['phoneNumber'], + subscriptionPlanId: parsedJson['subscriptionPlanId'], + subscriptionExpiryDate: parsedJson['subscriptionExpiryDate'], + subscriptionTotalOrders: parsedJson['subscriptionTotalOrders'], + subscriptionPlan: parsedJson['subscription_plan'] != null ? SubscriptionPlanModel.fromJson(parsedJson['subscription_plan']) : null, + ); + } + + Map toJson() { + photos.toList().removeWhere((element) => element == null); + Map json = { + 'author': author, + 'authorName': authorName, + 'sectionId': sectionId, + 'price': price, + 'disPrice': disPrice, + 'authorProfilePic': authorProfilePic, + 'subCategoryId': subCategoryId, + 'categoryId': categoryId, + 'createdAt': createdAt, + "g": geoFireData.toJson(), + 'description': description, + 'id': id, + 'latitude': latitude, + 'longitude': longitude, + 'photos': photos, + 'address': address, + 'reviewsCount': reviewsCount, + 'reviewsSum': reviewsSum, + 'title': title, + 'publish': publish, + 'startTime': startTime, + 'endTime': endTime, + 'priceUnit': priceUnit, + 'days': days, + 'phoneNumber': phoneNumber, + 'subscriptionPlanId': subscriptionPlanId, + 'subscriptionExpiryDate': subscriptionExpiryDate, + 'subscriptionTotalOrders': subscriptionTotalOrders, + 'subscription_plan': subscriptionPlan?.toJson(), + }; + return json; + } +} + +class GeoFireData { + String? geohash; + GeoPoint? geoPoint; + + GeoFireData({this.geohash, this.geoPoint}); + + factory GeoFireData.fromJson(Map parsedJson) { + return GeoFireData( + geohash: parsedJson['geohash'] ?? '', + geoPoint: parsedJson['geopoint'] ?? '', + ); + } + + Map toJson() { + return { + 'geohash': geohash, + 'geopoint': geoPoint, + }; + } +} + +class GeoPointClass { + double latitude; + + double longitude; + + GeoPointClass({this.latitude = 0.01, this.longitude = 0.01}); + + factory GeoPointClass.fromJson(Map parsedJson) { + return GeoPointClass( + latitude: parsedJson['latitude'] ?? 0.01, + longitude: parsedJson['longitude'] ?? 0.01, + ); + } + + Map toJson() { + return { + 'latitude': latitude, + 'longitude': longitude, + }; + } +} diff --git a/lib/models/rating_model.dart b/lib/models/rating_model.dart new file mode 100644 index 0000000..ad33d34 --- /dev/null +++ b/lib/models/rating_model.dart @@ -0,0 +1,68 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class RatingModel { + String? id; + double? rating; + List? photos; + String? comment; + String? orderId; + String? customerId; + String? vendorId; + String? productId; + String? driverId; + String? uname; + String? profile; + Map? reviewAttributes; + Timestamp? createdAt; + + RatingModel({ + this.id, + this.comment, + this.photos, + this.rating, + this.orderId, + this.vendorId, + this.productId, + this.driverId, + this.customerId, + this.uname, + this.createdAt, + this.reviewAttributes, + this.profile, + }); + + factory RatingModel.fromJson(Map parsedJson) { + return RatingModel( + comment: parsedJson['comment'], + photos: parsedJson['photos'] ?? [], + rating: double.parse(parsedJson['rating'].toString()), + id: parsedJson['Id'], + orderId: parsedJson['orderid'], + vendorId: parsedJson['VendorId'], + productId: parsedJson['productId'], + driverId: parsedJson['driverId'], + customerId: parsedJson['CustomerId'], + uname: parsedJson['uname'], + reviewAttributes: parsedJson['reviewAttributes'] ?? {}, + createdAt: parsedJson['createdAt'] ?? Timestamp.now(), + profile: parsedJson['profile']); + } + + Map toJson() { + return { + 'comment': comment, + 'photos': photos, + 'rating': rating, + 'Id': id, + 'orderid': orderId, + 'VendorId': vendorId, + 'productId': productId, + 'driverId': driverId, + 'CustomerId': customerId, + 'uname': uname, + 'profile': profile, + 'reviewAttributes': reviewAttributes ?? {}, + 'createdAt': createdAt + }; + } +} diff --git a/lib/models/referral_model.dart b/lib/models/referral_model.dart new file mode 100644 index 0000000..e25d86b --- /dev/null +++ b/lib/models/referral_model.dart @@ -0,0 +1,21 @@ +class ReferralModel { + String? id; + String? referralCode; + String? referralBy; + + ReferralModel({this.id,this.referralCode, this.referralBy}); + + ReferralModel.fromJson(Map json) { + id = json['id']; + referralCode = json['referralCode']; + referralBy = json['referralBy']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['referralCode'] = referralCode; + data['referralBy'] = referralBy; + return data; + } +} diff --git a/lib/models/rental_order_model.dart b/lib/models/rental_order_model.dart new file mode 100644 index 0000000..7e03d8c --- /dev/null +++ b/lib/models/rental_order_model.dart @@ -0,0 +1,194 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/rental_package_model.dart'; +import 'package:customer/models/rental_vehicle_type.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; + +class RentalOrderModel { + String? status; + List? rejectedByDrivers; + String? couponId; + Timestamp? bookingDateTime; + bool? paymentStatus; + String? discount; + String? authorID; + Timestamp? createdAt; + String? adminCommissionType; + String? sourceLocationName; + List? taxSetting; + String? id; + String? adminCommission; + String? couponCode; + String? sectionId; + String? tipAmount; + String? vehicleId; + String? paymentMethod; + RentalVehicleType? rentalVehicleType; + RentalPackageModel? rentalPackageModel; + String? otpCode; + DestinationLocation? sourceLocation; + UserModel? author; + UserModel? driver; + String? driverId; + String? subTotal; + Timestamp? startTime; + Timestamp? endTime; + String? startKitoMetersReading; + String? endKitoMetersReading; + String? zoneId; + G? sourcePoint; + + + RentalOrderModel({ + this.status, + this.rejectedByDrivers, + this.bookingDateTime, + this.paymentStatus, + this.discount, + this.authorID, + this.createdAt, + this.adminCommissionType, + this.sourceLocationName, + this.taxSetting, + this.id, + this.adminCommission, + this.couponCode, + this.couponId, + this.sectionId, + this.tipAmount, + this.vehicleId, + this.paymentMethod, + this.rentalVehicleType, + this.rentalPackageModel, + this.otpCode, + this.sourceLocation, + this.author, + this.subTotal, + this.driver, + this.driverId, + this.startTime, + this.endTime, + this.startKitoMetersReading, + this.endKitoMetersReading, + this.zoneId, + this.sourcePoint, + + }); + + RentalOrderModel.fromJson(Map json) { + status = json['status']; + rejectedByDrivers = json['rejectedByDrivers'] ?? []; + couponId = json['couponId']; + bookingDateTime = json['bookingDateTime']; + paymentStatus = json['paymentStatus']; + discount = json['discount'] == null ? "0.0" : json['discount'].toString(); + authorID = json['authorID']; + createdAt = json['createdAt']; + adminCommissionType = json['adminCommissionType']; + sourceLocationName = json['sourceLocationName']; + if (json['taxSetting'] != null) { + taxSetting = []; + json['taxSetting'].forEach((v) { + taxSetting!.add(TaxModel.fromJson(v)); + }); + } + id = json['id']; + adminCommission = json['adminCommission']; + couponCode = json['couponCode']; + sectionId = json['sectionId']; + tipAmount = json['tip_amount']; + vehicleId = json['vehicleId']; + paymentMethod = json['paymentMethod']; + rentalVehicleType = json['rentalVehicleType'] != null ? RentalVehicleType.fromJson(json['rentalVehicleType']) : null; + rentalPackageModel = json['rentalPackageModel'] != null ? RentalPackageModel.fromJson(json['rentalPackageModel']) : null; + otpCode = json['otpCode']; + sourceLocation = json['sourceLocation'] != null ? DestinationLocation.fromJson(json['sourceLocation']) : null; + author = json['author'] != null ? UserModel.fromJson(json['author']) : null; + subTotal = json['subTotal']; + driver = json['driver'] != null ? UserModel.fromJson(json['driver']) : null; + driverId = json['driverId']; + startTime = json['startTime']; + endTime = json['endTime']; + startKitoMetersReading = json['startKitoMetersReading'] ?? "0.0"; + endKitoMetersReading = json['endKitoMetersReading'] ?? "0.0"; + zoneId = json['zoneId']; + sourcePoint = json['sourcePoint'] != null ? G.fromJson(json['sourcePoint']) : null; + + } + + Map toJson() { + final Map data = {}; + data['status'] = status; + if (rejectedByDrivers != null) { + data['rejectedByDrivers'] = rejectedByDrivers!.map((v) => v.toJson()).toList(); + } + data['couponId'] = couponId; + data['bookingDateTime'] = bookingDateTime; + data['paymentStatus'] = paymentStatus; + data['discount'] = discount; + data['authorID'] = authorID; + data['createdAt'] = createdAt; + data['adminCommissionType'] = adminCommissionType; + data['sourceLocationName'] = sourceLocationName; + if (taxSetting != null) { + data['taxSetting'] = taxSetting!.map((v) => v.toJson()).toList(); + } + data['id'] = id; + data['adminCommission'] = adminCommission; + data['couponCode'] = couponCode; + data['sectionId'] = sectionId; + data['tip_amount'] = tipAmount; + data['vehicleId'] = vehicleId; + data['paymentMethod'] = paymentMethod; + data['driverId'] = driverId; + if (driver != null) { + data['driver'] = driver!.toJson(); + } + + if (rentalVehicleType != null) { + data['rentalVehicleType'] = rentalVehicleType!.toJson(); + } + + if (rentalPackageModel != null) { + data['rentalPackageModel'] = rentalPackageModel!.toJson(); + } + data['otpCode'] = otpCode; + if (sourceLocation != null) { + data['sourceLocation'] = sourceLocation!.toJson(); + } + if (author != null) { + data['author'] = author!.toJson(); + } + data['subTotal'] = subTotal; + data['startTime'] = startTime; + data['endTime'] = endTime; + data['startKitoMetersReading'] = startKitoMetersReading; + data['endKitoMetersReading'] = endKitoMetersReading; + data['zoneId'] = zoneId; + if (sourcePoint != null) { + data['sourcePoint'] = sourcePoint!.toJson(); + } + return data; + } +} + + +class DestinationLocation { + double? longitude; + double? latitude; + + DestinationLocation({this.longitude, this.latitude}); + + DestinationLocation.fromJson(Map json) { + longitude = json['longitude']; + latitude = json['latitude']; + } + + Map toJson() { + final Map data = {}; + data['longitude'] = longitude; + data['latitude'] = latitude; + return data; + } +} diff --git a/lib/models/rental_package_model.dart b/lib/models/rental_package_model.dart new file mode 100644 index 0000000..92db523 --- /dev/null +++ b/lib/models/rental_package_model.dart @@ -0,0 +1,62 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class RentalPackageModel { + String? id; + String? vehicleTypeId; + String? description; + String? ordering; + bool? published; + String? extraKmFare; + String? includedHours; + String? extraMinuteFare; + String? baseFare; + Timestamp? createdAt; + String? name; + String? includedDistance; + + RentalPackageModel( + {this.id, + this.vehicleTypeId, + this.description, + this.ordering, + this.published, + this.extraKmFare, + this.includedHours, + this.extraMinuteFare, + this.baseFare, + this.createdAt, + this.name, + this.includedDistance}); + + RentalPackageModel.fromJson(Map json) { + id = json['id']; + vehicleTypeId = json['vehicleTypeId']; + description = json['description']; + ordering = json['ordering']; + published = json['published']; + extraKmFare = json['extraKmFare']; + includedHours = json['includedHours']; + extraMinuteFare = json['extraMinuteFare']; + baseFare = json['baseFare']; + createdAt = json['createdAt']; + name = json['name']; + includedDistance = json['includedDistance']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['vehicleTypeId'] = vehicleTypeId; + data['description'] = description; + data['ordering'] = ordering; + data['published'] = published; + data['extraKmFare'] = extraKmFare; + data['includedHours'] = includedHours; + data['extraMinuteFare'] = extraMinuteFare; + data['baseFare'] = baseFare; + data['createdAt'] = createdAt; + data['name'] = name; + data['includedDistance'] = includedDistance; + return data; + } +} diff --git a/lib/models/rental_vehicle_type.dart b/lib/models/rental_vehicle_type.dart new file mode 100644 index 0000000..ffeca4b --- /dev/null +++ b/lib/models/rental_vehicle_type.dart @@ -0,0 +1,36 @@ +class RentalVehicleType { + String? rentalVehicleIcon; + String? shortDescription; + String? name; + String? description; + String? id; + bool? isActive; + String? supportedVehicle; + String? capacity; + + RentalVehicleType({this.rentalVehicleIcon, this.shortDescription, this.name, this.description, this.id, this.isActive, this.supportedVehicle, this.capacity}); + + RentalVehicleType.fromJson(Map json) { + rentalVehicleIcon = json['rental_vehicle_icon']; + shortDescription = json['short_description']; + name = json['name']; + description = json['description']; + id = json['id']; + isActive = json['isActive']; + supportedVehicle = json['supported_vehicle']; + capacity = json['capacity']; + } + + Map toJson() { + final Map data = {}; + data['rental_vehicle_icon'] = rentalVehicleIcon; + data['short_description'] = shortDescription; + data['name'] = name; + data['description'] = description; + data['id'] = id; + data['isActive'] = isActive; + data['supported_vehicle'] = supportedVehicle; + data['capacity'] = capacity; + return data; + } +} diff --git a/lib/models/review_attribute_model.dart b/lib/models/review_attribute_model.dart new file mode 100644 index 0000000..a2f2402 --- /dev/null +++ b/lib/models/review_attribute_model.dart @@ -0,0 +1,21 @@ +class ReviewAttributeModel { + String? id; + String? title; + + ReviewAttributeModel({ + this.id, + this.title, + }); + + ReviewAttributeModel.fromJson(Map json) { + id = json['id'] ?? ""; + title = json['title'] ?? ""; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + return data; + } +} diff --git a/lib/models/section_model.dart b/lib/models/section_model.dart new file mode 100644 index 0000000..909dfae --- /dev/null +++ b/lib/models/section_model.dart @@ -0,0 +1,91 @@ + +import 'package:customer/models/admin_commission_model.dart'; + +class SectionModel { + String? referralAmount; + String? serviceType; + String? color; + String? name; + String? sectionImage; + String? markerIcon; + String? id; + bool? isActive; + bool? dineInActive; + bool? isProductDetails; + String? serviceTypeFlag; + String? delivery_charge; + String? rideType; + String? theme; + int? nearByRadius; + AdminCommission? adminCommision; + + SectionModel({ + this.referralAmount, + this.serviceType, + this.color, + this.name, + this.sectionImage, + this.markerIcon, + this.id, + this.isActive, + this.theme, + this.adminCommision, + this.dineInActive, + this.delivery_charge, + this.nearByRadius, + this.isProductDetails, + this.serviceTypeFlag, + this.rideType, + }); + + SectionModel.fromJson(Map json) { + referralAmount = json['referralAmount'] ?? ''; + serviceType = json['serviceType'] ?? ''; + color = json['color']; + name = json['name']; + sectionImage = json['sectionImage']; + markerIcon = json['markerIcon']; + id = json['id']; + adminCommision = json.containsKey('adminCommision') + ? AdminCommission.fromJson(json['adminCommision']) + : null; + isActive = json['isActive']; + theme = json['theme'] ?? "theme_2"; + dineInActive = json['dine_in_active'] ?? false; + isProductDetails = json['is_product_details'] ?? false; + serviceTypeFlag = json['serviceTypeFlag'] ?? ''; + delivery_charge = json['delivery_charge'] ?? ''; + rideType = json['rideType'] ?? 'ride'; + + // 👇 Safe parsing for number (handles NaN, double, int) + final rawRadius = json['nearByRadius']; + if (rawRadius == null || rawRadius is! num || rawRadius.isNaN) { + nearByRadius = 5000; + } else { + nearByRadius = rawRadius.toInt(); + } + } + + Map toJson() { + final Map data = {}; + data['referralAmount'] = referralAmount; + data['serviceType'] = serviceType; + data['color'] = color; + data['name'] = name; + data['sectionImage'] = sectionImage; + data['markerIcon'] = markerIcon; + data['rideType'] = rideType; + data['theme'] = theme; + if (adminCommision != null) { + data['adminCommision'] = adminCommision!.toJson(); + } + data['id'] = id; + data['isActive'] = isActive; + data['dine_in_active'] = dineInActive; + data['is_product_details'] = isProductDetails; + data['serviceTypeFlag'] = serviceTypeFlag; + data['delivery_charge'] = delivery_charge; + data['nearByRadius'] = nearByRadius; + return data; + } +} diff --git a/lib/models/special_discount_model.dart b/lib/models/special_discount_model.dart new file mode 100644 index 0000000..0a58f24 --- /dev/null +++ b/lib/models/special_discount_model.dart @@ -0,0 +1,53 @@ +class SpecialDiscountModel { + String? day; + List? timeslot; + + SpecialDiscountModel({this.day, this.timeslot}); + + SpecialDiscountModel.fromJson(Map json) { + day = json['day']; + if (json['timeslot'] != null) { + timeslot = []; + json['timeslot'].forEach((v) { + timeslot!.add(Timeslot.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['day'] = day; + if (timeslot != null) { + data['timeslot'] = timeslot!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Timeslot { + String? from; + String? to; + String? discount; + String? type; + String? discount_type; + + Timeslot({this.from, this.to, this.discount, this.type}); + + Timeslot.fromJson(Map json) { + from = json['from']; + to = json['to']; + discount = json['discount']; + type = json['type']; + discount_type = json['discount_type']; + } + + Map toJson() { + final Map data = {}; + data['from'] = from; + data['to'] = to; + data['discount'] = discount; + data['type'] = type; + data['discount_type'] = discount_type; + return data; + } +} diff --git a/lib/models/story_model.dart b/lib/models/story_model.dart new file mode 100644 index 0000000..2d17caa --- /dev/null +++ b/lib/models/story_model.dart @@ -0,0 +1,26 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class StoryModel { + String? videoThumbnail; + List videoUrl = []; + String? vendorID; + Timestamp? createdAt; + + StoryModel({this.videoThumbnail, this.videoUrl = const [], this.vendorID, this.createdAt}); + + StoryModel.fromJson(Map json) { + videoThumbnail = json['videoThumbnail'] ?? ''; + videoUrl = json['videoUrl'] ?? []; + vendorID = json['vendorID'] ?? ''; + createdAt = json['createdAt'] ?? Timestamp.now(); + } + + Map toJson() { + final Map data = {}; + data['videoThumbnail'] = videoThumbnail; + data['videoUrl'] = videoUrl; + data['vendorID'] = vendorID; + data['createdAt'] = createdAt; + return data; + } +} diff --git a/lib/models/subscription_plan_model.dart b/lib/models/subscription_plan_model.dart new file mode 100644 index 0000000..f929fe0 --- /dev/null +++ b/lib/models/subscription_plan_model.dart @@ -0,0 +1,112 @@ + +import 'package:cloud_firestore/cloud_firestore.dart'; + +class SubscriptionPlanModel { + Timestamp? createdAt; + String? description; + String? expiryDay; + Features? features; + String? id; + bool? isEnable; + bool? isCommissionPlan; + String? itemLimit; + String? orderLimit; + String? name; + String? price; + String? place; + String? image; + String? type; + String? sectionId; + List? planPoints; + + SubscriptionPlanModel( + {this.createdAt, + this.description, + this.expiryDay, + this.features, + this.id, + this.isEnable, + this.isCommissionPlan, + this.itemLimit, + this.orderLimit, + this.name, + this.price, + this.place, + this.image, + this.type, + this.sectionId, + this.planPoints}); + + factory SubscriptionPlanModel.fromJson(Map json) { + return SubscriptionPlanModel( + createdAt: json['createdAt'], + description: json['description'], + expiryDay: json['expiryDay'], + features: json['features'] == null ? null : Features.fromJson(json['features']), + id: json['id'], + isEnable: json['isEnable'], + isCommissionPlan: json['isCommissionPlan'], + itemLimit: json['itemLimit'], + orderLimit: json['orderLimit'], + name: json['name'], + price: json['price'], + // place: json['place'], + sectionId: json['sectionId'], + image: json['image'], + type: json['type'], + planPoints: json['plan_points'] == null ? [] : List.from(json['plan_points']), + ); + } + + Map toJson() { + return { + 'createdAt': createdAt, + 'description': description, + 'expiryDay': expiryDay.toString(), + 'features': features?.toJson(), + 'id': id, + 'isEnable': isEnable, + 'itemLimit': itemLimit.toString(), + 'orderLimit': orderLimit.toString(), + 'name': name, + 'price': price.toString(), + 'place': place.toString(), + 'image': image.toString(), + 'type': type, + 'sectionId': sectionId, + 'plan_points': planPoints + }; + } +} + +class Features { + bool? chat; + bool? qrCodeGenerate; + bool? ownerMobileApp; + bool? demo; + + Features({ + this.chat, + this.qrCodeGenerate, + this.ownerMobileApp, + this.demo, + }); + + // Factory constructor to create an instance from JSON + factory Features.fromJson(Map json) { + return Features( + chat: json['chat'] ?? false, + qrCodeGenerate: json['qrCodeGenerate'] ?? false, + ownerMobileApp: json['ownerMobileApp'] ?? false, + ); + } + + // Method to convert an instance to JSON + Map toJson() { + return { + 'chat': chat, + 'qrCodeGenerate': qrCodeGenerate, + 'ownerMobileApp': ownerMobileApp, + }; + } +} diff --git a/lib/models/tax_model.dart b/lib/models/tax_model.dart new file mode 100644 index 0000000..dc2a00c --- /dev/null +++ b/lib/models/tax_model.dart @@ -0,0 +1,42 @@ +class TaxModel { + String? country; + bool? enable; + String? tax; + String? id; + String? type; + String? title; + String? sectionId; + + + + TaxModel({this.country, this.enable, this.tax, this.id, this.type, this.title,this.sectionId,}); + + TaxModel.fromJson(Map json) { + + + + + country = json['country']; + enable = json['enable']; + tax = json['tax']; + id = json['id']; + type = json['type']; + title = json['title']; + sectionId = json['sectionId']; + } + + Map toJson() { + final Map data = {}; + data['country'] = country; + data['enable'] = enable; + data['tax'] = tax; + data['id'] = id; + data['type'] = type; + data['title'] = title; + data['sectionId'] = sectionId; + + + + return data; + } +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 0000000..a1af720 --- /dev/null +++ b/lib/models/user_model.dart @@ -0,0 +1,569 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cab_order_model.dart'; + +import 'subscription_plan_model.dart'; +import 'admin_commission_model.dart'; + +class UserModel { + String? id; + String? firstName; + String? lastName; + String? email; + String? profilePictureURL; + String? fcmToken; + String? countryCode; + String? phoneNumber; + num? walletAmount; + bool? active; + bool? isActive; + bool? isDocumentVerify; + Timestamp? createdAt; + String? role; + UserLocation? location; + UserBankDetails? userBankDetails; + List? shippingAddress; + String? carName; + String? carNumber; + String? carPictureURL; + List? inProgressOrderID; + List? orderRequestData; + String? vendorID; + String? zoneId; + num? rotation; + String? appIdentifier; + String? provider; + String? subscriptionPlanId; + Timestamp? subscriptionExpiryDate; + SubscriptionPlanModel? subscriptionPlan; + String? serviceType; + String? sectionId; + String? vehicleId; + String? vehicleType; + String? carMakes; + String? reviewsCount; + String? reviewsSum; + AdminCommission? adminCommissionModel; + CabOrderModel? orderCabRequestData; + String? rideType; + String? ownerId; + bool? isOwner; + + UserModel({ + this.id, + this.firstName, + this.lastName, + this.active, + this.isActive, + this.isDocumentVerify, + this.email, + this.profilePictureURL, + this.fcmToken, + this.countryCode, + this.phoneNumber, + this.walletAmount, + this.createdAt, + this.role, + this.location, + this.shippingAddress, + this.carName, + this.carNumber, + this.carPictureURL, + this.inProgressOrderID, + this.orderRequestData, + this.vendorID, + this.zoneId, + this.rotation, + this.appIdentifier, + this.provider, + this.subscriptionPlanId, + this.subscriptionExpiryDate, + this.subscriptionPlan, + this.serviceType, + this.sectionId, + this.vehicleId, + this.vehicleType, + this.carMakes, + this.reviewsCount, + this.reviewsSum, + this.adminCommissionModel, + this.orderCabRequestData, + this.rideType, + this.ownerId, + this.isOwner, + }); + + String fullName() { + return "${firstName ?? ''} ${lastName ?? ''}"; + } + + double get averageRating { + final double sum = double.tryParse(reviewsSum ?? '0') ?? 0.0; + final double count = double.tryParse(reviewsCount ?? '0') ?? 0.0; + + if (count <= 0) return 0.0; + return sum / count; + } + + UserModel.fromJson(Map json) { + id = json['id']; + email = json['email']; + firstName = json['firstName']; + lastName = json['lastName']; + profilePictureURL = json['profilePictureURL']; + fcmToken = json['fcmToken']; + countryCode = json['countryCode']; + phoneNumber = json['phoneNumber']; + walletAmount = json['wallet_amount'] ?? 0; + createdAt = json['createdAt']; + active = json['active']; + isActive = json['isActive']; + isDocumentVerify = json['isDocumentVerify'] ?? false; + role = json['role'] ?? 'user'; + location = json['location'] != null ? UserLocation.fromJson(json['location']) : null; + userBankDetails = json['userBankDetails'] != null ? UserBankDetails.fromJson(json['userBankDetails']) : null; + if (json['shippingAddress'] != null) { + shippingAddress = []; + json['shippingAddress'].forEach((v) { + shippingAddress!.add(ShippingAddress.fromJson(v)); + }); + } + carName = json['carName']; + carNumber = json['carNumber']; + carPictureURL = json['carPictureURL']; + inProgressOrderID = json['inProgressOrderID'] ?? []; + //orderRequestData = json['orderRequestData'] ?? []; + if (json['orderRequestData'] is List) { + orderRequestData = json['orderRequestData']; + } else if (json['orderRequestData'] is Map) { + orderRequestData = [json['orderRequestData']]; + } else { + orderRequestData = []; + } + vendorID = json['vendorID'] ?? ''; + zoneId = json['zoneId'] ?? ''; + rotation = json['rotation']; + appIdentifier = json['appIdentifier']; + provider = json['provider']; + subscriptionPlanId = json['subscriptionPlanId']; + subscriptionExpiryDate = json['subscriptionExpiryDate']; + subscriptionPlan = json['subscription_plan'] != null ? SubscriptionPlanModel.fromJson(json['subscription_plan']) : null; + serviceType = json['serviceType']; + sectionId = json['sectionId'] ?? ''; + vehicleId = json['vehicleId']; + vehicleType = json['vehicleType']; + carMakes = json['carMakes']; + reviewsCount = json['reviewsCount'] == null ? '0' : json['reviewsCount'].toString(); + reviewsSum = json['reviewsSum'] == null ? '0' : json['reviewsSum'].toString(); + adminCommissionModel = json['adminCommission'] != null ? AdminCommission.fromJson(json['adminCommission']) : null; + orderCabRequestData = json['ordercabRequestData'] != null ? CabOrderModel.fromJson(json['ordercabRequestData']) : null; + rideType = json['rideType']; + ownerId = json['ownerId']; + isOwner = json['isOwner']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['email'] = email; + data['firstName'] = firstName; + data['lastName'] = lastName; + data['profilePictureURL'] = profilePictureURL; + data['fcmToken'] = fcmToken; + data['countryCode'] = countryCode; + data['phoneNumber'] = phoneNumber; + data['wallet_amount'] = walletAmount ?? 0; + data['createdAt'] = createdAt; + data['active'] = active; + data['isActive'] = isActive ?? false; + data['role'] = role; + data['isDocumentVerify'] = isDocumentVerify; + data['zoneId'] = zoneId; + data['sectionId'] = sectionId ?? ''; + + if (location != null) { + data['location'] = location!.toJson(); + } + if (userBankDetails != null) { + data['userBankDetails'] = userBankDetails!.toJson(); + } + if (shippingAddress != null) { + data['shippingAddress'] = shippingAddress!.map((v) => v.toJson()).toList(); + } + data['serviceType'] = serviceType; + data['rotation'] = rotation; + data['inProgressOrderID'] = inProgressOrderID; + + if (role == Constant.userRoleDriver) { + data['vendorID'] = vendorID; + data['carName'] = carName; + data['carNumber'] = carNumber; + data['carPictureURL'] = carPictureURL; + data['orderRequestData'] = orderRequestData; + + data['vehicleType'] = vehicleType; + data['carMakes'] = carMakes; + data['vehicleId'] = vehicleId ?? ''; + if (orderCabRequestData != null) { + data['ordercabRequestData'] = orderCabRequestData!.toJson(); + } + data['rideType'] = rideType; + data['ownerId'] = ownerId; + data['isOwner'] = isOwner; + } + if (role == Constant.userRoleVendor) { + data['vendorID'] = vendorID; + data['subscriptionPlanId'] = subscriptionPlanId; + data['subscriptionExpiryDate'] = subscriptionExpiryDate; + data['subscription_plan'] = subscriptionPlan?.toJson(); + } + data['appIdentifier'] = appIdentifier; + data['provider'] = provider; + data['reviewsCount'] = reviewsCount; + data['reviewsSum'] = reviewsSum; + if (adminCommissionModel != null) { + data['adminCommission'] = adminCommissionModel!.toJson(); + } + return data; + } +} + +class UserLocation { + double? latitude; + double? longitude; + + UserLocation({this.latitude, this.longitude}); + + UserLocation.fromJson(Map json) { + latitude = json['latitude']; + longitude = json['longitude']; + } + + Map toJson() { + final Map data = {}; + data['latitude'] = latitude; + data['longitude'] = longitude; + return data; + } +} + +class ShippingAddress { + String? id; + String? address; + String? addressAs; + String? landmark; + String? locality; + UserLocation? location; + bool? isDefault; + + ShippingAddress({this.address, this.landmark, this.locality, this.location, this.isDefault, this.addressAs, this.id}); + + ShippingAddress.fromJson(Map json) { + id = json['id']; + address = json['address']; + landmark = json['landmark']; + locality = json['locality']; + isDefault = json['isDefault']; + addressAs = json['addressAs']; + location = json['location'] == null ? null : UserLocation.fromJson(json['location']); + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['address'] = address; + data['landmark'] = landmark; + data['locality'] = locality; + data['isDefault'] = isDefault; + data['addressAs'] = addressAs; + if (location != null) { + data['location'] = location!.toJson(); + } + return data; + } + + // String getFullAddress() { + // return '${address == null || address!.isEmpty ? "" : address} $locality ${landmark == null || landmark!.isEmpty ? "" : landmark.toString()}'; + // } + + String getFullAddress() { + return [ + if (address != null && address!.trim().isNotEmpty) address!.trim(), + if (locality != null && locality!.trim().isNotEmpty) locality!.trim(), + if (landmark != null && landmark!.trim().isNotEmpty) landmark!.trim(), + ].join(', ').replaceAll(RegExp(r'\s+'), ' ').trim(); + } +} + +class UserBankDetails { + String bankName; + String branchName; + String holderName; + String accountNumber; + String otherDetails; + + UserBankDetails({this.bankName = '', this.otherDetails = '', this.branchName = '', this.accountNumber = '', this.holderName = ''}); + + factory UserBankDetails.fromJson(Map parsedJson) { + return UserBankDetails( + bankName: parsedJson['bankName'] ?? '', + branchName: parsedJson['branchName'] ?? '', + holderName: parsedJson['holderName'] ?? '', + accountNumber: parsedJson['accountNumber'] ?? '', + otherDetails: parsedJson['otherDetails'] ?? '', + ); + } + + Map toJson() { + return { + 'bankName': bankName, + 'branchName': branchName, + 'holderName': holderName, + 'accountNumber': accountNumber, + 'otherDetails': otherDetails, + }; + } +} + +class CarInfo { + String? passenger; + String? doors; + String? airConditioning; + String? gear; + String? mileage; + String? fuelFilling; + String? fuelType; + String? maxPower; + String? mph; + String? topSpeed; + List? carImage; + + CarInfo({ + this.passenger, + this.doors, + this.airConditioning, + this.gear, + this.mileage, + this.fuelFilling, + this.fuelType, + this.carImage, + this.maxPower, + this.mph, + this.topSpeed, + }); + + CarInfo.fromJson(Map json) { + passenger = json['passenger'] ?? ""; + doors = json['doors'] ?? ""; + airConditioning = json['air_conditioning'] ?? ""; + gear = json['gear'] ?? ""; + mileage = json['mileage'] ?? ""; + fuelFilling = json['fuel_filling'] ?? ""; + fuelType = json['fuel_type'] ?? ""; + carImage = json['car_image'] ?? []; + maxPower = json['maxPower'] ?? ""; + mph = json['mph'] ?? ""; + topSpeed = json['topSpeed'] ?? ""; + } + + Map toJson() { + final Map data = {}; + data['passenger'] = passenger; + data['doors'] = doors; + data['air_conditioning'] = airConditioning; + data['gear'] = gear; + data['mileage'] = mileage; + data['fuel_filling'] = fuelFilling; + data['fuel_type'] = fuelType; + data['car_image'] = carImage; + data['maxPower'] = maxPower; + data['mph'] = mph; + data['topSpeed'] = topSpeed; + return data; + } +} + +class UserSettings { + bool pushNewMessages; + + bool orderUpdates; + + bool newArrivals; + + bool promotions; + + UserSettings({this.pushNewMessages = true, this.orderUpdates = true, this.newArrivals = true, this.promotions = true}); + + factory UserSettings.fromJson(Map parsedJson) { + return UserSettings( + pushNewMessages: parsedJson['pushNewMessages'] ?? true, + orderUpdates: parsedJson['orderUpdates'] ?? true, + newArrivals: parsedJson['newArrivals'] ?? true, + promotions: parsedJson['promotions'] ?? true, + ); + } + + Map toJson() { + return {'pushNewMessages': pushNewMessages, 'orderUpdates': orderUpdates, 'newArrivals': newArrivals, 'promotions': promotions}; + } +} + +// import 'package:cloud_firestore/cloud_firestore.dart'; +// import 'package:customer/constant/constant.dart'; +// +// import 'subscription_plan_model.dart'; +// +// class UserModel { +// String? id; +// String? firstName; +// String? lastName; +// String? email; +// String? profilePictureURL; +// String? fcmToken; +// String? countryCode; +// String? phoneNumber; +// num? walletAmount; +// bool? active; +// bool? isActive; +// bool? isDocumentVerify; +// Timestamp? createdAt; +// String? role; +// UserLocation? location; +// UserBankDetails? userBankDetails; +// List? shippingAddress; +// String? carName; +// String? carNumber; +// String? carPictureURL; +// List? inProgressOrderID; +// List? orderRequestData; +// String? vendorID; +// String? zoneId; +// num? rotation; +// String? appIdentifier; +// String? provider; +// String? subscriptionPlanId; +// Timestamp? subscriptionExpiryDate; +// SubscriptionPlanModel? subscriptionPlan; +// +// UserModel({ +// this.id, +// this.firstName, +// this.lastName, +// this.active, +// this.isActive, +// this.isDocumentVerify, +// this.email, +// this.profilePictureURL, +// this.fcmToken, +// this.countryCode, +// this.phoneNumber, +// this.walletAmount, +// this.createdAt, +// this.role, +// this.location, +// this.shippingAddress, +// this.carName, +// this.carNumber, +// this.carPictureURL, +// this.inProgressOrderID, +// this.orderRequestData, +// this.vendorID, +// this.zoneId, +// this.rotation, +// this.appIdentifier, +// this.provider, +// this.subscriptionPlanId, +// this.subscriptionExpiryDate, +// this.subscriptionPlan, +// }); +// +// String fullName() { +// return "${firstName ?? ''} ${lastName ?? ''}"; +// } +// +// UserModel.fromJson(Map json) { +// id = json['id']; +// email = json['email']; +// firstName = json['firstName']; +// lastName = json['lastName']; +// profilePictureURL = json['profilePictureURL']; +// fcmToken = json['fcmToken']; +// countryCode = json['countryCode']; +// phoneNumber = json['phoneNumber']; +// walletAmount = json['wallet_amount'] ?? 0; +// createdAt = json['createdAt']; +// active = json['active']; +// isActive = json['isActive']; +// isDocumentVerify = json['isDocumentVerify'] ?? false; +// role = json['role'] ?? 'user'; +// location = json['location'] != null ? UserLocation.fromJson(json['location']) : null; +// userBankDetails = json['userBankDetails'] != null ? UserBankDetails.fromJson(json['userBankDetails']) : null; +// if (json['shippingAddress'] != null) { +// shippingAddress = []; +// json['shippingAddress'].forEach((v) { +// shippingAddress!.add(ShippingAddress.fromJson(v)); +// }); +// } +// carName = json['carName']; +// carNumber = json['carNumber']; +// carPictureURL = json['carPictureURL']; +// inProgressOrderID = json['inProgressOrderID']; +// orderRequestData = json['orderRequestData']; +// vendorID = json['vendorID'] ?? ''; +// zoneId = json['zoneId'] ?? ''; +// rotation = json['rotation']; +// appIdentifier = json['appIdentifier']; +// provider = json['provider']; +// subscriptionPlanId = json['subscriptionPlanId']; +// subscriptionExpiryDate = json['subscriptionExpiryDate']; +// subscriptionPlan = json['subscription_plan'] != null ? SubscriptionPlanModel.fromJson(json['subscription_plan']) : null; +// } +// +// Map toJson() { +// final Map data = {}; +// data['id'] = id; +// data['email'] = email; +// data['firstName'] = firstName; +// data['lastName'] = lastName; +// data['profilePictureURL'] = profilePictureURL; +// data['fcmToken'] = fcmToken; +// data['countryCode'] = countryCode; +// data['phoneNumber'] = phoneNumber; +// data['wallet_amount'] = walletAmount ?? 0; +// data['createdAt'] = createdAt; +// data['active'] = active; +// data['isActive'] = isActive; +// data['role'] = role; +// data['isDocumentVerify'] = isDocumentVerify; +// data['zoneId'] = zoneId; +// if (location != null) { +// data['location'] = location!.toJson(); +// } +// if (userBankDetails != null) { +// data['userBankDetails'] = userBankDetails!.toJson(); +// } +// if (shippingAddress != null) { +// data['shippingAddress'] = shippingAddress!.map((v) => v.toJson()).toList(); +// } +// if (role == Constant.userRoleDriver) { +// data['vendorID'] = vendorID; +// data['carName'] = carName; +// data['carNumber'] = carNumber; +// data['carPictureURL'] = carPictureURL; +// data['inProgressOrderID'] = inProgressOrderID; +// data['orderRequestData'] = orderRequestData; +// data['rotation'] = rotation; +// } +// if (role == Constant.userRoleVendor) { +// data['vendorID'] = vendorID; +// data['subscriptionPlanId'] = subscriptionPlanId; +// data['subscriptionExpiryDate'] = subscriptionExpiryDate; +// data['subscription_plan'] = subscriptionPlan?.toJson(); +// } +// data['appIdentifier'] = appIdentifier; +// data['provider'] = provider; +// +// return data; +// } +// } +// diff --git a/lib/models/variant_info.dart b/lib/models/variant_info.dart new file mode 100644 index 0000000..462a687 --- /dev/null +++ b/lib/models/variant_info.dart @@ -0,0 +1,27 @@ +class VariantInfo { + String? variantId; + String? variantPrice; + String? variantSku; + String? variant_image; + Map? variant_options; + + VariantInfo({this.variantId, this.variantPrice, this.variant_image, this.variantSku, this.variant_options}); + + VariantInfo.fromJson(Map json) { + variantId = json['variantId'] ?? ''; + variantPrice = json['variantPrice'] ?? ''; + variantSku = json['variantSku'] ?? ''; + variant_image = json['variant_image'] ?? ''; + variant_options = json['variant_options'] ?? {}; + } + + Map toJson() { + final Map data = {}; + data['variantId'] = variantId; + data['variantPrice'] = variantPrice; + data['variantSku'] = variantSku; + data['variant_image'] = variant_image; + data['variant_options'] = variant_options; + return data; + } +} diff --git a/lib/models/vehicle_type.dart b/lib/models/vehicle_type.dart new file mode 100644 index 0000000..3250147 --- /dev/null +++ b/lib/models/vehicle_type.dart @@ -0,0 +1,56 @@ +class VehicleType { + String? shortDescription; + String? vehicleIcon; + String? name; + String? description; + String? id; + bool? isActive; + String? capacity; + String? supportedVehicle; + num? delivery_charges_per_km; + num? minimum_delivery_charges; + num? minimum_delivery_charges_within_km; + + VehicleType( + {this.shortDescription, + this.vehicleIcon, + this.name, + this.description, + this.id, + this.isActive, + this.capacity, + this.delivery_charges_per_km, + this.minimum_delivery_charges, + this.minimum_delivery_charges_within_km, + this.supportedVehicle}); + + VehicleType.fromJson(Map json) { + shortDescription = json['short_description']; + vehicleIcon = json['vehicle_icon']; + name = json['name']; + description = json['description']; + id = json['id']; + isActive = json['isActive']; + capacity = json['capacity']; + delivery_charges_per_km = json['delivery_charges_per_km'] ?? 0.0; + minimum_delivery_charges = json['minimum_delivery_charges'] ?? 0.0; + minimum_delivery_charges_within_km = json['minimum_delivery_charges_within_km'] ?? 0.0; + supportedVehicle = json['supported_vehicle']; + } + + Map toJson() { + final Map data = {}; + data['short_description'] = shortDescription; + data['vehicle_icon'] = vehicleIcon; + data['name'] = name; + data['description'] = description; + data['id'] = id; + data['isActive'] = isActive; + data['capacity'] = capacity; + data['supported_vehicle'] = supportedVehicle; + data['delivery_charges_per_km'] = delivery_charges_per_km; + data['minimum_delivery_charges'] = minimum_delivery_charges; + data['minimum_delivery_charges_within_km'] = minimum_delivery_charges_within_km; + return data; + } +} diff --git a/lib/models/vendor_category_model.dart b/lib/models/vendor_category_model.dart new file mode 100644 index 0000000..ebab826 --- /dev/null +++ b/lib/models/vendor_category_model.dart @@ -0,0 +1,27 @@ +class VendorCategoryModel { + List? reviewAttributes; + String? photo; + String? description; + String? id; + String? title; + + VendorCategoryModel({this.reviewAttributes, this.photo, this.description, this.id, this.title}); + + VendorCategoryModel.fromJson(Map json) { + reviewAttributes = json['review_attributes'] ?? []; + photo = json['photo'] ?? ""; + description = json['description'] ?? ''; + id = json['id'] ?? ""; + title = json['title'] ?? ""; + } + + Map toJson() { + final Map data = {}; + data['review_attributes'] = reviewAttributes; + data['photo'] = photo; + data['description'] = description; + data['id'] = id; + data['title'] = title; + return data; + } +} diff --git a/lib/models/vendor_model.dart b/lib/models/vendor_model.dart new file mode 100644 index 0000000..8794e98 --- /dev/null +++ b/lib/models/vendor_model.dart @@ -0,0 +1,404 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/subscription_plan_model.dart'; + +import 'admin_commission_model.dart'; + +class VendorModel { + String? author; + bool? dineInActive; + String? openDineTime; + List? categoryID; + String? id; + String? categoryPhoto; + List? restaurantMenuPhotos; + List? workingHours; + String? location; + String? fcmToken; + G? g; + bool? hidephotos; + bool? reststatus; + Filters? filters; + AdminCommission? adminCommission; + String? photo; + String? description; + num? walletAmount; + String? closeDineTime; + String? zoneId; + Timestamp? createdAt; + double? longitude; + bool? enabledDiveInFuture; + String? restaurantCost; + DeliveryCharge? deliveryCharge; + String? authorProfilePic; + String? authorName; + String? phonenumber; + List? specialDiscount; + bool? specialDiscountEnable; + GeoPoint? coordinates; + num? reviewsSum; + num? reviewsCount; + List? photos; + String? title; + List? categoryTitle; + double? latitude; + String? subscriptionPlanId; + Timestamp? subscriptionExpiryDate; + SubscriptionPlanModel? subscriptionPlan; + String? subscriptionTotalOrders; + String? sectionId; + bool? isSelfDelivery; + + VendorModel({ + this.author, + this.dineInActive, + this.openDineTime, + this.categoryID, + this.id, + this.categoryPhoto, + this.restaurantMenuPhotos, + this.workingHours, + this.location, + this.fcmToken, + this.g, + this.hidephotos, + this.reststatus, + this.filters, + this.reviewsCount, + this.photo, + this.description, + this.walletAmount, + this.closeDineTime, + this.zoneId, + this.createdAt, + this.longitude, + this.enabledDiveInFuture, + this.restaurantCost, + this.deliveryCharge, + this.adminCommission, + this.authorProfilePic, + this.authorName, + this.phonenumber, + this.specialDiscount, + this.specialDiscountEnable, + this.coordinates, + this.reviewsSum, + this.photos, + this.title, + this.categoryTitle, + this.latitude, + this.subscriptionPlanId, + this.subscriptionExpiryDate, + this.subscriptionPlan, + this.subscriptionTotalOrders, + this.sectionId, + this.isSelfDelivery, + }); + + VendorModel.fromJson(Map json) { + author = json['author']; + dineInActive = json['dine_in_active']; + openDineTime = json['openDineTime']; + categoryID = json['categoryID'] is String ? [] : json['categoryID'] ?? []; + id = json['id']; + categoryPhoto = json['categoryPhoto']; + restaurantMenuPhotos = json['restaurantMenuPhotos'] ?? []; + if (json['workingHours'] != null) { + workingHours = []; + json['workingHours'].forEach((v) { + workingHours!.add(WorkingHours.fromJson(v)); + }); + } + location = json['location']; + fcmToken = json['fcmToken']; + g = json['g'] != null ? G.fromJson(json['g']) : null; + hidephotos = json['hidephotos']; + reststatus = json['reststatus']; + filters = json['filters'] != null ? Filters.fromJson(json['filters']) : null; + reviewsCount = num.parse('${json['reviewsCount'] ?? 0.0}'); + photo = json['photo']; + description = json['description']; + walletAmount = json['walletAmount']; + closeDineTime = json['closeDineTime']; + zoneId = json['zoneId']; + // createdAt = json['createdAt']; + longitude = double.parse(json['longitude'].toString()); + enabledDiveInFuture = json['enabledDiveInFuture']; + restaurantCost = json['restaurantCost']?.toString(); + deliveryCharge = json['DeliveryCharge'] != null ? DeliveryCharge.fromJson(json['DeliveryCharge']) : null; + adminCommission = json['adminCommission'] != null ? AdminCommission.fromJson(json['adminCommission']) : null; + authorProfilePic = json['authorProfilePic']; + authorName = json['authorName']; + phonenumber = json['phonenumber']; + if (json['specialDiscount'] != null) { + specialDiscount = []; + json['specialDiscount'].forEach((v) { + specialDiscount!.add(SpecialDiscount.fromJson(v)); + }); + } + specialDiscountEnable = json['specialDiscountEnable']; + coordinates = json['coordinates']; + reviewsSum = num.parse('${json['reviewsSum'] ?? 0.0}'); + photos = json['photos'] ?? []; + title = json['title']; + categoryTitle = json['categoryTitle'] is String ? [] : json['categoryTitle'] ?? []; + latitude = double.parse(json['latitude'].toString()); + subscriptionPlanId = json['subscriptionPlanId']; + // subscriptionExpiryDate = json['subscriptionExpiryDate']; + subscriptionPlan = json['subscription_plan'] != null ? SubscriptionPlanModel.fromJson(json['subscription_plan']) : null; + subscriptionTotalOrders = json['subscriptionTotalOrders']; + sectionId = json['section_id']; + isSelfDelivery = json['isSelfDelivery'] ?? false; + createdAt = + json['createdAt'] is Timestamp + ? json['createdAt'] + : json['createdAt'] != null + ? Timestamp.fromMillisecondsSinceEpoch((json['createdAt']['_seconds'] ?? 0) * 1000) + : null; + subscriptionExpiryDate = + json['subscriptionExpiryDate'] is Timestamp + ? json['subscriptionExpiryDate'] + : json['subscriptionExpiryDate'] != null + ? Timestamp.fromMillisecondsSinceEpoch((json['subscriptionExpiryDate']['_seconds'] ?? 0) * 1000) + : null; + } + + Map toJson() { + final Map data = {}; + data['author'] = author; + data['dine_in_active'] = dineInActive; + data['openDineTime'] = openDineTime; + data['categoryID'] = categoryID; + data['id'] = id; + data['categoryPhoto'] = categoryPhoto; + data['restaurantMenuPhotos'] = restaurantMenuPhotos; + data['subscriptionPlanId'] = subscriptionPlanId; + data['subscriptionExpiryDate'] = subscriptionExpiryDate; + data['subscription_plan'] = subscriptionPlan?.toJson(); + data['subscriptionTotalOrders'] = subscriptionTotalOrders; + data['section_id'] = sectionId; + if (workingHours != null) { + data['workingHours'] = workingHours!.map((v) => v.toJson()).toList(); + } + data['location'] = location; + data['fcmToken'] = fcmToken; + if (g != null) { + data['g'] = g!.toJson(); + } + data['hidephotos'] = hidephotos; + data['reststatus'] = reststatus; + if (filters != null) { + data['filters'] = filters!.toJson(); + } + data['reviewsCount'] = reviewsCount; + data['photo'] = photo; + data['description'] = description; + data['walletAmount'] = walletAmount; + data['closeDineTime'] = closeDineTime; + data['zoneId'] = zoneId; + data['createdAt'] = createdAt; + data['longitude'] = longitude; + data['enabledDiveInFuture'] = enabledDiveInFuture; + data['restaurantCost'] = restaurantCost; + if (deliveryCharge != null) { + data['DeliveryCharge'] = deliveryCharge!.toJson(); + } + if (adminCommission != null) { + data['adminCommission'] = adminCommission!.toJson(); + } + data['authorProfilePic'] = authorProfilePic; + data['authorName'] = authorName; + data['phonenumber'] = phonenumber; + if (specialDiscount != null) { + data['specialDiscount'] = specialDiscount!.map((v) => v.toJson()).toList(); + } + data['specialDiscountEnable'] = specialDiscountEnable; + data['coordinates'] = coordinates; + data['reviewsSum'] = reviewsSum; + data['photos'] = photos; + data['title'] = title; + data['categoryTitle'] = categoryTitle; + data['latitude'] = latitude; + data['isSelfDelivery'] = isSelfDelivery ?? false; + return data; + } +} + +class WorkingHours { + String? day; + List? timeslot; + + WorkingHours({this.day, this.timeslot}); + + WorkingHours.fromJson(Map json) { + day = json['day']; + if (json['timeslot'] != null) { + timeslot = []; + json['timeslot'].forEach((v) { + timeslot!.add(Timeslot.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['day'] = day; + if (timeslot != null) { + data['timeslot'] = timeslot!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Timeslot { + String? to; + String? from; + + Timeslot({this.to, this.from}); + + Timeslot.fromJson(Map json) { + to = json['to']; + from = json['from']; + } + + Map toJson() { + final Map data = {}; + data['to'] = to; + data['from'] = from; + return data; + } +} + +class G { + String? geohash; + GeoPoint? geopoint; + + G({this.geohash, this.geopoint}); + + G.fromJson(Map json) { + geohash = json['geohash']; + geopoint = json['geopoint']; + } + + Map toJson() { + final Map data = {}; + data['geohash'] = geohash; + data['geopoint'] = geopoint; + return data; + } +} + +class Filters { + String? goodForLunch; + String? outdoorSeating; + String? liveMusic; + String? vegetarianFriendly; + String? goodForDinner; + String? goodForBreakfast; + String? freeWiFi; + String? takesReservations; + + Filters({this.goodForLunch, this.outdoorSeating, this.liveMusic, this.vegetarianFriendly, this.goodForDinner, this.goodForBreakfast, this.freeWiFi, this.takesReservations}); + + Filters.fromJson(Map json) { + goodForLunch = json['Good for Lunch']; + outdoorSeating = json['Outdoor Seating']; + liveMusic = json['Live Music']; + vegetarianFriendly = json['Vegetarian Friendly']; + goodForDinner = json['Good for Dinner']; + goodForBreakfast = json['Good for Breakfast']; + freeWiFi = json['Free Wi-Fi']; + takesReservations = json['Takes Reservations']; + } + + Map toJson() { + final Map data = {}; + data['Good for Lunch'] = goodForLunch; + data['Outdoor Seating'] = outdoorSeating; + data['Live Music'] = liveMusic; + data['Vegetarian Friendly'] = vegetarianFriendly; + data['Good for Dinner'] = goodForDinner; + data['Good for Breakfast'] = goodForBreakfast; + data['Free Wi-Fi'] = freeWiFi; + data['Takes Reservations'] = takesReservations; + return data; + } +} + +class DeliveryCharge { + num? minimumDeliveryChargesWithinKm; + num? minimumDeliveryCharges; + num? deliveryChargesPerKm; + bool? vendorCanModify; + + DeliveryCharge({this.minimumDeliveryChargesWithinKm, this.minimumDeliveryCharges, this.deliveryChargesPerKm, this.vendorCanModify}); + + DeliveryCharge.fromJson(Map json) { + minimumDeliveryChargesWithinKm = json['minimum_delivery_charges_within_km']; + minimumDeliveryCharges = json['minimum_delivery_charges']; + deliveryChargesPerKm = json['delivery_charges_per_km']; + vendorCanModify = json['vendor_can_modify']; + } + + Map toJson() { + final Map data = {}; + data['minimum_delivery_charges_within_km'] = minimumDeliveryChargesWithinKm; + data['minimum_delivery_charges'] = minimumDeliveryCharges; + data['delivery_charges_per_km'] = deliveryChargesPerKm; + data['vendor_can_modify'] = vendorCanModify; + return data; + } +} + +class SpecialDiscount { + String? day; + List? timeslot; + + SpecialDiscount({this.day, this.timeslot}); + + SpecialDiscount.fromJson(Map json) { + day = json['day']; + if (json['timeslot'] != null) { + timeslot = []; + json['timeslot'].forEach((v) { + timeslot!.add(SpecialDiscountTimeslot.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['day'] = day; + if (timeslot != null) { + data['timeslot'] = timeslot!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class SpecialDiscountTimeslot { + String? discount; + String? discountType; + String? to; + String? type; + String? from; + + SpecialDiscountTimeslot({this.discount, this.discountType, this.to, this.type, this.from}); + + SpecialDiscountTimeslot.fromJson(Map json) { + discount = json['discount']; + discountType = json['discount_type']; + to = json['to']; + type = json['type']; + from = json['from']; + } + + Map toJson() { + final Map data = {}; + data['discount'] = discount; + data['discount_type'] = discountType; + data['to'] = to; + data['type'] = type; + data['from'] = from; + return data; + } +} diff --git a/lib/models/wallet_transaction_model.dart b/lib/models/wallet_transaction_model.dart new file mode 100644 index 0000000..c5c470e --- /dev/null +++ b/lib/models/wallet_transaction_model.dart @@ -0,0 +1,58 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class WalletTransactionModel { + String? userId; + String? paymentMethod; + double? amount; + bool? isTopup; + String? orderId; + String? paymentStatus; + Timestamp? date; + String? id; + String? transactionUser; + String? note; + String? serviceType; + + WalletTransactionModel({ + this.userId, + this.paymentMethod, + this.amount, + this.isTopup, + this.orderId, + this.paymentStatus, + this.date, + this.id, + this.transactionUser, + this.note, + this.serviceType, + }); + + WalletTransactionModel.fromJson(Map json) { + id = json['id']; + userId = json['user_id']; + paymentMethod = json['payment_method']; + amount = double.parse("${json['amount'] ?? 0.0}"); + isTopup = json['isTopUp']; + orderId = json['order_id']; + paymentStatus = json['payment_status']; + date = json['date']; + transactionUser = json['transactionUser'] ?? 'customer'; + note = json['note'] ?? 'Wallet Top-up'; + serviceType = json['serviceType']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['user_id'] = userId; + data['payment_method'] = paymentMethod; + data['amount'] = amount; + data['isTopUp'] = isTopup; + data['order_id'] = orderId; + data['payment_status'] = paymentStatus; + data['date'] = date; + data['transactionUser'] = transactionUser; + data['note'] = note; + return data; + } +} diff --git a/lib/models/worker_model.dart b/lib/models/worker_model.dart new file mode 100644 index 0000000..3db702c --- /dev/null +++ b/lib/models/worker_model.dart @@ -0,0 +1,105 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/models/provider_serivce_model.dart'; + +class WorkerModel { + String? id; + String? firstName; + String? lastName; + String? email; + String? phoneNumber; + String? address; + String? salary; + Timestamp? createdAt; + GeoFireData geoFireData; + double? latitude; + double? longitude; + String? providerId; + bool? active; + String fcmToken; + String profilePictureURL; + bool? online; + num? reviewsCount; + num? reviewsSum; + + WorkerModel({ + this.id = '', + this.firstName = '', + this.lastName = '', + this.email = '', + this.phoneNumber = '', + this.address = '', + this.salary, + this.createdAt, + geoFireData, + this.latitude = 0.1, + this.longitude = 0.1, + this.providerId, + this.active = false, + this.fcmToken = '', + this.profilePictureURL = '', + this.online, + this.reviewsCount = 0 , + this.reviewsSum = 0, + }): geoFireData = geoFireData ?? + GeoFireData( + geohash: "", + geoPoint: const GeoPoint(0.0, 0.0), + ); + + + String fullName() { + return '$firstName $lastName'; + } + + factory WorkerModel.fromJson(Map parsedJson) { + return WorkerModel( + id: parsedJson['id'] ?? '', + firstName: parsedJson['firstName'] ?? '', + lastName: parsedJson['lastName'] ?? '', + email: parsedJson['email'] ?? '', + phoneNumber: parsedJson['phoneNumber'] ?? '', + address: parsedJson['address'] ?? '', + salary: parsedJson['salary'] ?? '', + createdAt: parsedJson['createdAt'] ?? Timestamp.now(), + geoFireData: parsedJson.containsKey('g') + ? GeoFireData.fromJson(parsedJson['g']) + : GeoFireData( + geohash: "", + geoPoint: const GeoPoint(0.0, 0.0), + ), + latitude: parsedJson['latitude'] ?? 0.1, + longitude: parsedJson['longitude'] ?? 0.1, + providerId: parsedJson['providerId'] ?? '', + active: parsedJson['active'] ?? false, + fcmToken: parsedJson['fcmToken'] ?? '', + profilePictureURL: parsedJson['profilePictureURL'] ?? '', + online: parsedJson['online'] ?? false, + reviewsCount: parsedJson['reviewsCount'] ?? 0, + reviewsSum: parsedJson['reviewsSum'] ?? 0, + ); + } + + Map toJson() { + Map json = { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'email': email, + 'phoneNumber': phoneNumber, + 'address': address, + 'salary': salary, + 'createdAt': createdAt, + "g": geoFireData.toJson(), + 'latitude': latitude, + 'longitude': longitude, + 'providerId': providerId, + 'active': active, + 'fcmToken': fcmToken, + 'profilePictureURL': profilePictureURL, + 'online': online, + 'reviewsCount': reviewsCount, + 'reviewsSum': reviewsSum, + }; + return json; + } +} diff --git a/lib/models/working_hours_model.dart b/lib/models/working_hours_model.dart new file mode 100644 index 0000000..000ff11 --- /dev/null +++ b/lib/models/working_hours_model.dart @@ -0,0 +1,49 @@ +class WorkingHoursModel { + String? day; + List? timeslot; + + WorkingHoursModel({this.day, this.timeslot}); + + WorkingHoursModel.fromJson(Map json) { + day = json['day']; + if (json['timeslot'] != null) { + timeslot = []; + json['timeslot'].forEach((v) { + timeslot!.add(Timeslot.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['day'] = day; + if (timeslot != null) { + data['timeslot'] = timeslot!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Timeslot { + String? from; + String? to; + + String? date; + + Timeslot({ + this.from, + this.to, + }); + + Timeslot.fromJson(Map json) { + from = json['from']; + to = json['to']; + } + + Map toJson() { + final Map data = {}; + data['from'] = from; + data['to'] = to; + return data; + } +} diff --git a/lib/models/zone_model.dart b/lib/models/zone_model.dart new file mode 100644 index 0000000..2b80374 --- /dev/null +++ b/lib/models/zone_model.dart @@ -0,0 +1,40 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class ZoneModel { + List? area; + bool? publish; + double? latitude; + String? name; + String? id; + double? longitude; + + ZoneModel({this.area, this.publish, this.latitude, this.name, this.id, this.longitude}); + + ZoneModel.fromJson(Map json) { + if (json['area'] != null) { + area = []; + json['area'].forEach((v) { + area!.add(v); + }); + } + + publish = json['publish']; + latitude = json['latitude']; + name = json['name']; + id = json['id']; + longitude = json['longitude']; + } + + Map toJson() { + final Map data = {}; + if (area != null) { + data['area'] = area!.map((v) => v).toList(); + } + data['publish'] = publish; + data['latitude'] = latitude; + data['name'] = name; + data['id'] = id; + data['longitude'] = longitude; + return data; + } +} diff --git a/lib/payment/MercadoPagoScreen.dart b/lib/payment/MercadoPagoScreen.dart new file mode 100644 index 0000000..ceb4282 --- /dev/null +++ b/lib/payment/MercadoPagoScreen.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:customer/constant/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class MercadoPagoScreen extends StatefulWidget { + final String initialURl; + + const MercadoPagoScreen({super.key, required this.initialURl}); + + @override + State createState() => _MercadoPagoScreenState(); +} + +class _MercadoPagoScreenState extends State { + WebViewController controller = WebViewController(); + + @override + void initState() { + initController(); + super.initState(); + } + + void initController() { + controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) {}, + onPageStarted: (String url) {}, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest navigation) async { + debugPrint("--->2 ${navigation.url}"); + if (navigation.url.contains("${Constant.globalUrl}payment/success")) { + Get.back(result: true); + } + if (navigation.url.contains("${Constant.globalUrl}payment/failure") || navigation.url.contains("${Constant.globalUrl}payment/pending")) { + Get.back(result: false); + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(widget.initialURl)); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + _showMyDialog(); + return false; + }, + child: Scaffold( + appBar: AppBar( + title: Text("Payment".tr), + centerTitle: false, + leading: GestureDetector( + onTap: () { + _showMyDialog(); + }, + child: const Icon(Icons.arrow_back), + ), + ), + body: WebViewWidget(controller: controller), + ), + ); + } + + Future _showMyDialog() async { + return showDialog( + context: context, + barrierDismissible: true, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: Text('Cancel Payment'.tr), + content: SingleChildScrollView(child: Text("Cancel Payment?".tr)), + actions: [ + TextButton( + child: Text('Cancel'.tr, style: const TextStyle(color: Colors.red)), + onPressed: () { + Navigator.of(context).pop(); + Get.back(result: false); + }, + ), + TextButton( + child: Text('Continue'.tr, style: const TextStyle(color: Colors.green)), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/payment/PayFastScreen.dart b/lib/payment/PayFastScreen.dart new file mode 100644 index 0000000..e4b570c --- /dev/null +++ b/lib/payment/PayFastScreen.dart @@ -0,0 +1,107 @@ +// ignore_for_file: file_names + +import 'dart:developer'; + +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class PayFastScreen extends StatefulWidget { + final String htmlData; + final PayFastModel payFastSettingData; + + const PayFastScreen({super.key, required this.htmlData, required this.payFastSettingData}); + + @override + State createState() => _PayFastScreenState(); +} + +class _PayFastScreenState extends State { + WebViewController controller = WebViewController(); + + @override + void initState() { + initController(); + super.initState(); + } + + void initController() { + controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + // Update loading bar. + }, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest navigation) async { + if (kDebugMode) { + log("--->2 $navigation"); + } + if (navigation.url == widget.payFastSettingData.returnUrl) { + Get.back(result: true); + } else if (navigation.url == widget.payFastSettingData.notifyUrl) { + Get.back(result: false); + } else if (navigation.url == widget.payFastSettingData.cancelUrl) { + _showMyDialog(); + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadHtmlString((widget.htmlData)); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + _showMyDialog(); + return false; + }, + child: Scaffold( + appBar: AppBar( + leading: GestureDetector( + onTap: () { + _showMyDialog(); + }, + child: const Icon(Icons.arrow_back), + ), + ), + body: WebViewWidget(controller: controller), + ), + ); + } + + Future _showMyDialog() async { + return showDialog( + context: context, + barrierDismissible: true, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Cancel Payment'), + content: SingleChildScrollView(child: Text("Cancel Payment?".tr)), + actions: [ + TextButton( + child: Text('Exit'.tr, style: TextStyle(color: Colors.red)), + onPressed: () { + Get.back(); + Get.back(result: false); + }, + ), + TextButton( + child: Text('Continue Payment'.tr, style: TextStyle(color: Colors.green)), + onPressed: () { + Get.back(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/payment/RazorPayFailedModel.dart b/lib/payment/RazorPayFailedModel.dart new file mode 100644 index 0000000..b2085c8 --- /dev/null +++ b/lib/payment/RazorPayFailedModel.dart @@ -0,0 +1,85 @@ +// To parse this JSON data, do +// +// final razorPayFailedModel = razorPayFailedModelFromJson(jsonString); + +import 'dart:convert'; + +RazorPayFailedModel razorPayFailedModelFromJson(String str) => RazorPayFailedModel.fromJson(json.decode(str)); + +String razorPayFailedModelToJson(RazorPayFailedModel data) => json.encode(data.toJson()); + +class RazorPayFailedModel { + RazorPayFailedModel({ + required this.error, + required this.httpStatusCode, + }); + + Error error; + int httpStatusCode; + + factory RazorPayFailedModel.fromJson(Map? json) => RazorPayFailedModel( + error: Error.fromJson(json!["error"]), + httpStatusCode: json["http_status_code"], + ); + + Map toJson() => { + "error": error.toJson(), + "http_status_code": httpStatusCode, + }; +} + +class Error { + Error({ + required this.code, + required this.description, + required this.source, + required this.step, + required this.reason, + required this.metadata, + }); + + String code; + String description; + String source; + String step; + String reason; + Metadata metadata; + + factory Error.fromJson(Map json) => Error( + code: json["code"] ?? '', + description: json["description"] ?? "", + source: json["source"] ?? "", + step: json["step"] ?? "", + reason: json["reason"] ?? "", + metadata: Metadata.fromJson(json["metadata"]), + ); + + Map toJson() => { + "code": code, + "description": description, + "source": source, + "step": step, + "reason": reason, + "metadata": metadata.toJson(), + }; +} + +class Metadata { + Metadata({ + required this.paymentId, + required this.orderId, + }); + + String paymentId; + String orderId; + + factory Metadata.fromJson(Map json) => Metadata( + paymentId: json["payment_id"] ?? "", + orderId: json["order_id"] ?? "", + ); + + Map toJson() => { + "payment_id": paymentId, + "order_id": orderId, + }; +} diff --git a/lib/payment/createRazorPayOrderModel.dart b/lib/payment/createRazorPayOrderModel.dart new file mode 100644 index 0000000..c4d9be8 --- /dev/null +++ b/lib/payment/createRazorPayOrderModel.dart @@ -0,0 +1,77 @@ +// To parse this JSON data, do +// +// final createRazorPayOrderModel = createRazorPayOrderModelFromJson(jsonString); + +import 'dart:convert'; + +CreateRazorPayOrderModel createRazorPayOrderModelFromJson(String str) => CreateRazorPayOrderModel.fromJson(json.decode(str)); + +String createRazorPayOrderModelToJson(CreateRazorPayOrderModel data) => json.encode(data.toJson()); + +class CreateRazorPayOrderModel { + CreateRazorPayOrderModel({ + required this.id, + required this.entity, + required this.amount, + required this.amountPaid, + required this.amountDue, + required this.currency, + required this.receipt, + required this.offerId, + required this.status, + required this.attempts, + required this.notes, + required this.createdAt, + }); + + String id; + String entity; + int amount; + int amountPaid; + int amountDue; + String currency; + String receipt; + dynamic offerId; + String status; + int attempts; + Notes notes; + int createdAt; + + factory CreateRazorPayOrderModel.fromJson(Map json) => CreateRazorPayOrderModel( + id: json["id"], + entity: json["entity"], + amount: json["amount"], + amountPaid: json["amount_paid"], + amountDue: json["amount_due"], + currency: json["currency"], + receipt: json["receipt"] ?? "", + offerId: json["offer_id"], + status: json["status"], + attempts: json["attempts"], + notes: Notes.fromJson(json["notes"]), + createdAt: json["created_at"], + ); + + Map toJson() => { + "id": id, + "entity": entity, + "amount": amount, + "amount_paid": amountPaid, + "amount_due": amountDue, + "currency": currency, + "receipt": receipt, + "offer_id": offerId, + "status": status, + "attempts": attempts, + "notes": notes.toJson(), + "created_at": createdAt, + }; +} + +class Notes { + Notes(); + + factory Notes.fromJson(Map json) => Notes(); + + Map toJson() => {}; +} diff --git a/lib/payment/getPaytmTxtToken.dart b/lib/payment/getPaytmTxtToken.dart new file mode 100644 index 0000000..b91d2db --- /dev/null +++ b/lib/payment/getPaytmTxtToken.dart @@ -0,0 +1,203 @@ +// To parse this JSON data, do +// +// final getPaymentTxtTokenModel = getPaymentTxtTokenModelFromJson(jsonString); + +import 'dart:convert'; + +GetPaymentTxtTokenModel getPaymentTxtTokenModelFromJson(String str) => GetPaymentTxtTokenModel.fromJson(json.decode(str)); + +String getPaymentTxtTokenModelToJson(GetPaymentTxtTokenModel data) => json.encode(data.toJson()); + +class GetPaymentTxtTokenModel { + GetPaymentTxtTokenModel({ + required this.head, + required this.body, + }); + + Head head; + Body body; + + factory GetPaymentTxtTokenModel.fromJson(Map json) => + GetPaymentTxtTokenModel( + head: Head.fromJson(json["head"] ?? {}), + body: Body.fromJson(json["body"] ?? {}), + ); + + Map toJson() => { + "head": head.toJson(), + "body": body.toJson(), + }; +} + +class Body { + Body({ + required this.resultInfo, + this.txnToken, + this.isPromoCodeValid, + this.authenticated, + }); + + ResultInfo resultInfo; + String? txnToken; + bool? isPromoCodeValid; + bool? authenticated; + + factory Body.fromJson(Map json) => Body( + resultInfo: ResultInfo.fromJson(json["resultInfo"] ?? {}), + txnToken: json["txnToken"], + isPromoCodeValid: json["isPromoCodeValid"], + authenticated: json["authenticated"], + ); + + Map toJson() => { + "resultInfo": resultInfo.toJson(), + "txnToken": txnToken, + "isPromoCodeValid": isPromoCodeValid, + "authenticated": authenticated, + }; +} + +class ResultInfo { + ResultInfo({ + this.resultStatus, + this.resultCode, + this.resultMsg, + }); + + String? resultStatus; + String? resultCode; + String? resultMsg; + + factory ResultInfo.fromJson(Map json) => ResultInfo( + resultStatus: json["resultStatus"], + resultCode: json["resultCode"], + resultMsg: json["resultMsg"], + ); + + Map toJson() => { + "resultStatus": resultStatus, + "resultCode": resultCode, + "resultMsg": resultMsg, + }; +} + +class Head { + Head({ + this.responseTimestamp, + this.version, + this.signature, + }); + + String? responseTimestamp; + String? version; + String? signature; + + factory Head.fromJson(Map json) => Head( + responseTimestamp: json["responseTimestamp"], + version: json["version"], + signature: json["signature"], + ); + + Map toJson() => { + "responseTimestamp": responseTimestamp, + "version": version, + "signature": signature, + }; +} + + +// class GetPaymentTxtTokenModel { +// GetPaymentTxtTokenModel({ +// required this.head, +// required this.body, +// }); +// +// Head head; +// Body body; +// +// factory GetPaymentTxtTokenModel.fromJson(Map json) => GetPaymentTxtTokenModel( +// head: Head.fromJson(json["head"]), +// body: Body.fromJson(json["body"]), +// ); +// +// Map toJson() => { +// "head": head.toJson(), +// "body": body.toJson(), +// }; +// } +// +// class Body { +// Body({ +// required this.resultInfo, +// required this.txnToken, +// required this.isPromoCodeValid, +// required this.authenticated, +// }); +// +// ResultInfo resultInfo; +// String txnToken; +// bool isPromoCodeValid; +// bool authenticated; +// +// factory Body.fromJson(Map json) => Body( +// resultInfo: ResultInfo.fromJson(json["resultInfo"]), +// txnToken: json["txnToken"], +// isPromoCodeValid: json["isPromoCodeValid"], +// authenticated: json["authenticated"], +// ); +// +// Map toJson() => { +// "resultInfo": resultInfo.toJson(), +// "txnToken": txnToken, +// "isPromoCodeValid": isPromoCodeValid, +// "authenticated": authenticated, +// }; +// } +// +// class ResultInfo { +// ResultInfo({ +// required this.resultStatus, +// required this.resultCode, +// required this.resultMsg, +// }); +// +// String resultStatus; +// String resultCode; +// String resultMsg; +// +// factory ResultInfo.fromJson(Map json) => ResultInfo( +// resultStatus: json["resultStatus"], +// resultCode: json["resultCode"], +// resultMsg: json["resultMsg"], +// ); +// +// Map toJson() => { +// "resultStatus": resultStatus, +// "resultCode": resultCode, +// "resultMsg": resultMsg, +// }; +// } +// +// class Head { +// Head({ +// required this.responseTimestamp, +// required this.version, +// required this.signature, +// }); +// +// String responseTimestamp; +// String version; +// String signature; +// +// factory Head.fromJson(Map json) => Head( +// responseTimestamp: json["responseTimestamp"], +// version: json["version"], +// signature: json["signature"], +// ); +// +// Map toJson() => { +// "responseTimestamp": responseTimestamp, +// "version": version, +// "signature": signature, +// }; +// } diff --git a/lib/payment/midtrans_screen.dart b/lib/payment/midtrans_screen.dart new file mode 100644 index 0000000..9660953 --- /dev/null +++ b/lib/payment/midtrans_screen.dart @@ -0,0 +1,115 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class MidtransScreen extends StatefulWidget { + final String initialURl; + + const MidtransScreen({super.key, required this.initialURl}); + + @override + State createState() => _MidtransScreenState(); +} + +class _MidtransScreenState extends State { + WebViewController controller = WebViewController(); + bool isLoading = true; + + @override + void initState() { + controller.clearCache(); + initController(); + super.initState(); + } + + void initController() { + controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: ((url) { + setState(() { + isLoading = false; + }); + }), + onNavigationRequest: (NavigationRequest navigation) async { + log("Midtrans :: ${navigation.url}"); + if (Platform.isIOS) { + if (navigation.url.contains('/success')) { + Get.back(result: true); + } else if (navigation.url.contains('/failed')) { + Get.back(result: false); + } + } else { + String? orderId = Uri.parse(navigation.url).queryParameters['merchant_order_id']; + if (orderId != null) { + Get.back(result: true); + } else { + Get.back(result: false); + } + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(widget.initialURl)); + } + + @override + Widget build(BuildContext context) { + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () async { + _showMyDialog(); + return false; + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + centerTitle: false, + leading: GestureDetector( + onTap: () { + _showMyDialog(); + }, + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + ), + body: Stack(alignment: Alignment.center, children: [WebViewWidget(controller: controller), Visibility(visible: isLoading, child: const Center(child: CircularProgressIndicator()))]), + ), + ); + } + + Future _showMyDialog() async { + return showDialog( + context: context, + barrierDismissible: true, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: Text('Cancel Payment'.tr), + content: SingleChildScrollView(child: Text("Cancel Payment?".tr)), + actions: [ + TextButton( + child: Text('Cancel'.tr, style: const TextStyle(color: Colors.red)), + onPressed: () { + Get.back(result: false); + Get.back(result: false); + }, + ), + TextButton( + child: Text('Continue'.tr, style: const TextStyle(color: Colors.green)), + onPressed: () { + Get.back(result: false); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/payment/orangePayScreen.dart b/lib/payment/orangePayScreen.dart new file mode 100644 index 0000000..d187080 --- /dev/null +++ b/lib/payment/orangePayScreen.dart @@ -0,0 +1,339 @@ +// ignore_for_file: must_be_immutable + +import 'dart:async'; +import 'dart:convert'; +import 'package:customer/models/payment_model/orange_money.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:webview_flutter/webview_flutter.dart'; + +class OrangeMoneyScreen extends StatefulWidget { + String initialURl; + OrangeMoney orangePay; + String accessToken = ''; + String payToken = ''; + String orderId = ''; + String amount = ''; + + OrangeMoneyScreen({super.key, required this.initialURl, required this.orangePay, required this.accessToken, required this.payToken, required this.orderId, required this.amount}); + + @override + State createState() => _OrangeMoneyScreenState(); +} + +class _OrangeMoneyScreenState extends State { + WebViewController controller = WebViewController(); + bool isLoading = true; + Timer? timer; + + @override + void initState() { + controller.clearCache(); + initController(); + startTransactionPolling(); + super.initState(); + } + + // 🔹 Poll Orange API every 3 seconds to check status + void startTransactionPolling() { + timer = Timer.periodic(const Duration(seconds: 3), (Timer t) async { + if (!mounted) return; + + String status = await transactionStatus(accessToken: widget.accessToken, amount: widget.amount, orderId: widget.orderId, payToken: widget.payToken); + + if (status == 'SUCCESS') { + timer?.cancel(); + debugPrint('✅ Payment successful for Order ID: ${widget.orderId}'); + Get.back(result: true); + } else if (status == 'FAILED' || status == 'CANCELLED') { + timer?.cancel(); + debugPrint('❌ Payment failed or cancelled.'); + Get.back(result: false); + } + }); + } + + void initController() { + controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (url) { + setState(() { + isLoading = false; + }); + }, + ), + ) + ..loadRequest(Uri.parse(widget.initialURl)); + } + + Future transactionStatus({required String orderId, required String amount, required String payToken, required String accessToken}) async { + String apiUrl = widget.orangePay.isSandbox == true ? 'https://api.orange.com/orange-money-webpay/dev/v1/transactionstatus' : 'https://api.orange.com/orange-money-webpay/cm/v1/transactionstatus'; + + Map requestBody = {"order_id": orderId, "amount": amount, "pay_token": payToken}; + + try { + var response = await http.post( + Uri.parse(apiUrl), + headers: {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', 'Accept': 'application/json'}, + body: json.encode(requestBody), + ); + + if (response.statusCode == 201) { + Map responseData = jsonDecode(response.body); + debugPrint('🔍 Transaction Status: ${responseData['status']}'); + return responseData['status']; + } else { + return ''; + } + } catch (e) { + debugPrint('⚠️ Transaction check error: $e'); + return ''; + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + _showCancelDialog(); + return false; + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + leading: IconButton(icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: _showCancelDialog), + title: Text('Orange Money Payment'.tr), + ), + body: isLoading ? const Center(child: CircularProgressIndicator()) : WebViewWidget(controller: controller), + ), + ); + } + + Future _showCancelDialog() async { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Cancel Payment'.tr), + content: Text('Are you sure you want to cancel this payment?'.tr), + actions: [ + TextButton(child: Text('No', style: TextStyle(color: Colors.green)), onPressed: () => Get.back()), + TextButton( + child: Text('Yes'.tr, style: TextStyle(color: Colors.red)), + onPressed: () { + timer?.cancel(); + Get.back(); // close dialog + Get.back(result: false); // close WebView and mark as failed + }, + ), + ], + ); + }, + ); + } + + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } +} + +// // ignore_for_file: must_be_immutable +// +// import 'dart:async'; +// import 'dart:convert'; +// import 'package:customer/models/payment_model/orange_money.dart'; +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:http/http.dart' as http; +// import 'package:webview_flutter/webview_flutter.dart'; +// +// class OrangeMoneyScreen extends StatefulWidget { +// String initialURl; +// OrangeMoney orangePay; +// String accessToken = ''; +// String payToken = ''; +// String orderId = ''; +// String amount = ''; +// +// OrangeMoneyScreen({ +// super.key, +// required this.initialURl, +// required this.orangePay, +// required this.accessToken, +// required this.payToken, +// required this.orderId, +// required this.amount, +// }); +// +// @override +// State createState() => _OrangeMoneyScreenState(); +// } +// +// class _OrangeMoneyScreenState extends State { +// WebViewController controller = WebViewController(); +// bool isLoading = true; +// +// @override +// void initState() { +// controller.clearCache(); +// initController(); +// callTransaction(); +// super.initState(); +// } +// +// Timer? timer; +// +// void callTransaction() { +// timer = Timer.periodic(const Duration(seconds: 3), (Timer t) { +// if (mounted) { +// transactionstatus(accessToken: widget.accessToken, amount: widget.amount, orderId: widget.orderId, payToken: widget.payToken).then((value) { +// if (value == 'SUCCESS') { +// if (timer != null) { +// timer!.cancel(); +// } +// Get.back(result: true); +// } else if (value == 'FAILED') { +// if (timer != null) { +// timer!.cancel(); +// } +// Get.back(result: false); +// } +// }); +// } +// }); +// } +// +// void initController() { +// controller = WebViewController() +// ..setJavaScriptMode(JavaScriptMode.unrestricted) +// ..setBackgroundColor(const Color(0x00000000)) +// ..setNavigationDelegate( +// NavigationDelegate( +// onPageFinished: ((url) { +// setState(() { +// isLoading = false; +// }); +// }), +// onNavigationRequest: (NavigationRequest navigation) async { +// return NavigationDecision.navigate; +// }, +// ), +// ) +// ..loadRequest(Uri.parse(widget.initialURl)); +// } +// +// Future transactionstatus({ +// required String orderId, +// required String amount, +// required String payToken, +// required String accessToken, +// }) async { +// String apiUrl = widget.orangePay.isSandbox == true +// ? 'https://api.orange.com/orange-money-webpay/dev/v1/transactionstatus' +// : 'https://api.orange.com/orange-money-webpay/cm/v1/transactionstatus'; +// Map requestBody = { +// "order_id": orderId, +// "amount": amount, // "OUV", +// "pay_token": payToken +// }; +// +// var response = await http.post(Uri.parse(apiUrl), +// headers: { +// 'Authorization': 'Bearer $accessToken', +// 'Content-Type': 'application/json', +// 'Accept': 'application/json', +// }, +// body: json.encode(requestBody)); +// +// // Handle the response +// if (response.statusCode == 201) { +// Map responseData = jsonDecode(response.body); +// return responseData['status']; +// } else { +// return ''; +// } +// } +// +// @override +// Widget build(BuildContext context) { +// // ignore: deprecated_member_use +// return WillPopScope( +// onWillPop: () async { +// _showMyDialog(); +// return false; +// }, +// child: Scaffold( +// appBar: AppBar( +// backgroundColor: Colors.black, +// centerTitle: false, +// leading: GestureDetector( +// onTap: () { +// _showMyDialog(); +// }, +// child: const Icon( +// Icons.arrow_back, +// color: Colors.white, +// ), +// )), +// body: isLoading +// ? const Center( +// child: CircularProgressIndicator(), +// ) +// : WebViewWidget(controller: controller), +// ), +// ); +// } +// +// Future _showMyDialog() async { +// return showDialog( +// context: context, +// barrierDismissible: true, // user must tap button! +// builder: (BuildContext context) { +// return AlertDialog( +// title: Text('Cancel Payment'.tr), +// content: SingleChildScrollView( +// child: Text("cancelPayment?".tr), +// ), +// actions: [ +// TextButton( +// child: Text( +// 'Cancel'.tr, +// style: const TextStyle(color: Colors.red), +// ), +// onPressed: () { +// Get.back(result: false); +// Get.back(result: false); +// }, +// ), +// TextButton( +// child: Text( +// 'Continue'.tr, +// style: const TextStyle(color: Colors.green), +// ), +// onPressed: () { +// Get.back(result: false); +// }, +// ), +// ], +// ); +// }, +// ); +// } +// +// @override +// void dispose() { +// if (timer != null) { +// timer!.cancel(); +// } +// // TODO: implement dispose +// super.dispose(); +// } +// } diff --git a/lib/payment/paystack/pay_stack_screen.dart b/lib/payment/paystack/pay_stack_screen.dart new file mode 100644 index 0000000..3ade273 --- /dev/null +++ b/lib/payment/paystack/pay_stack_screen.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:customer/payment/paystack/paystack_url_genrater.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class PayStackScreen extends StatefulWidget { + final String initialURl; + final String reference; + final String amount; + final String secretKey; + final String callBackUrl; + + const PayStackScreen({super.key, required this.initialURl, required this.reference, required this.amount, required this.secretKey, required this.callBackUrl}); + + @override + State createState() => _PayStackScreenState(); +} + +class _PayStackScreenState extends State { + WebViewController controller = WebViewController(); + + @override + void initState() { + initController(); + super.initState(); + } + + void initController() { + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + onPageFinished: (String url) {}, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest navigation) async { + debugPrint("--->2${navigation.url}"); + debugPrint("--->2" "${widget.callBackUrl}?trxref=${widget.reference}&reference=${widget.reference}"); + if (navigation.url == 'https://foodieweb.siswebapp.com/success?trxref=${widget.reference}&reference=${widget.reference}' || + navigation.url == '${widget.callBackUrl}?trxref=${widget.reference}&reference=${widget.reference}') { + final isDone = await PayStackURLGen.verifyTransaction(secretKey: widget.secretKey, reference: widget.reference, amount: widget.amount); + Get.back(result: isDone); + } + if ((navigation.url == '${widget.callBackUrl}?trxref=${widget.reference}&reference=${widget.reference}') || + (navigation.url == "https://hello.pstk.xyz/callback") || + (navigation.url == 'https://standard.paystack.co/close') || + (navigation.url == 'https://talazo.app/login')) { + final isDone = await PayStackURLGen.verifyTransaction(secretKey: widget.secretKey, reference: widget.reference, amount: widget.amount); + Get.back(result: isDone); + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(widget.initialURl)); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + _showMyDialog(); + return false; + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: AppThemeData.grey50, + title: Text("Payment".tr), + centerTitle: false, + leading: GestureDetector( + onTap: () { + _showMyDialog(); + }, + child: const Icon( + Icons.arrow_back, + ), + )), + body: WebViewWidget(controller: controller), + ), + ); + } + + Future _showMyDialog() async { + return showDialog( + context: context, + barrierDismissible: true, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Cancel Payment'), + content: const SingleChildScrollView( + child: Text("cancelPayment?"), + ), + actions: [ + TextButton( + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.red), + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text( + 'Continue', + style: TextStyle(color: Colors.green), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/payment/paystack/pay_stack_url_model.dart b/lib/payment/paystack/pay_stack_url_model.dart new file mode 100644 index 0000000..555516c --- /dev/null +++ b/lib/payment/paystack/pay_stack_url_model.dart @@ -0,0 +1,57 @@ +// To parse this JSON data, do +// +// final payStackUrlModel = payStackUrlModelFromJson(jsonString); + +import 'dart:convert'; + +PayStackUrlModel payStackUrlModelFromJson(String str) => PayStackUrlModel.fromJson(json.decode(str)); + +String payStackUrlModelToJson(PayStackUrlModel data) => json.encode(data.toJson()); + +class PayStackUrlModel { + PayStackUrlModel({ + required this.status, + required this.message, + required this.data, + }); + + bool status; + String message; + Data data; + + factory PayStackUrlModel.fromJson(Map json) => PayStackUrlModel( + status: json["status"], + message: json["message"], + data: Data.fromJson(json["data"]), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": data.toJson(), + }; +} + +class Data { + Data({ + required this.authorizationUrl, + required this.accessCode, + required this.reference, + }); + + String authorizationUrl; + String accessCode; + String reference; + + factory Data.fromJson(Map json) => Data( + authorizationUrl: json["authorization_url"], + accessCode: json["access_code"], + reference: json["reference"], + ); + + Map toJson() => { + "authorization_url": authorizationUrl, + "access_code": accessCode, + "reference": reference, + }; +} diff --git a/lib/payment/paystack/paystack_url_genrater.dart b/lib/payment/paystack/paystack_url_genrater.dart new file mode 100644 index 0000000..a169a94 --- /dev/null +++ b/lib/payment/paystack/paystack_url_genrater.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:customer/models/payment_model/pay_fast_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/payment/paystack/pay_stack_url_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; + +class PayStackURLGen { + static Future payStackURLGen({required String amount, required String secretKey, required String currency, required UserModel userModel}) async { + const url = "https://api.paystack.co/transaction/initialize"; + final response = await http.post(Uri.parse(url), body: { + "email": userModel.email, + "amount": amount, + "currency": currency, + }, headers: { + "Authorization": "Bearer $secretKey", + }); + debugPrint(response.body); + final data = jsonDecode(response.body); + if (!data["status"]) { + return null; + } + return PayStackUrlModel.fromJson(data); + } + + static Future verifyTransaction({ + required String reference, + required String secretKey, + required String amount, + }) async { + debugPrint("we Enter payment Settle"); + debugPrint(reference); + + final url = "https://api.paystack.co/transaction/verify/$reference"; + + var response = await http.get(Uri.parse(url), headers: { + "Authorization": "Bearer $secretKey", + }); + + debugPrint(response.body); + final data = jsonDecode(response.body); + if (data["status"] == true) { + if (data["message"] == "Verification successful") {} + } + + return data["status"]; + + //PayPalClientSettleModel.fromJson(data); + } + + static Future getPayHTML({required String amount, required PayFastModel payFastSettingData, required UserModel userModel}) async { + String newUrl = 'https://${payFastSettingData.isSandbox == false ? "www" : "sandbox"}.payfast.co.za/eng/process'; + Map body = { + 'merchant_id': payFastSettingData.merchantId, + 'merchant_key': payFastSettingData.merchantKey, + 'amount': amount, + 'item_name': "goRide online payment", + 'return_url': payFastSettingData.returnUrl, + 'cancel_url': payFastSettingData.cancelUrl, + 'notify_url': payFastSettingData.notifyUrl, + 'name_first': userModel.firstName, + 'name_last': userModel.lastName, + 'email_address': userModel.email, + }; + + final response = await http.post( + Uri.parse(newUrl), + body: body, + ); + + debugPrint(response.body); + return response.body; + } +} diff --git a/lib/payment/rozorpayConroller.dart b/lib/payment/rozorpayConroller.dart new file mode 100644 index 0000000..ce74654 --- /dev/null +++ b/lib/payment/rozorpayConroller.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import 'package:customer/models/payment_model/razorpay_model.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:http/http.dart' as http; + +import '../constant/constant.dart'; + +class RazorPayController { + Future createOrderRazorPay({required double amount, required RazorPayModel? razorpayModel}) async { + final String orderId = DateTime.now().millisecondsSinceEpoch.toString(); + RazorPayModel razorPayData = razorpayModel!; + print(razorPayData.razorpayKey); + print("we Enter In"); + const url = "${Constant.globalUrl}payments/razorpay/createorder"; + print(orderId); + final response = await http.post( + Uri.parse(url), + body: { + "amount": (amount.round() * 100).toString(), + "receipt_id": orderId, + "currency": "INR", + "razorpaykey": razorPayData.razorpayKey, + "razorPaySecret": razorPayData.razorpaySecret, + "isSandBoxEnabled": razorPayData.isSandboxEnabled.toString(), + }, + ); + + if (response.statusCode == 500) { + return null; + } else { + final data = jsonDecode(response.body); + print(data); + + return CreateRazorPayOrderModel.fromJson(data); + } + } +} diff --git a/lib/payment/stripe_failed_model.dart b/lib/payment/stripe_failed_model.dart new file mode 100644 index 0000000..1b0fdbd --- /dev/null +++ b/lib/payment/stripe_failed_model.dart @@ -0,0 +1,61 @@ +// To parse this JSON data, do +// +// final stripePayFailedModel = stripePayFailedModelFromJson(jsonString); + +import 'dart:convert'; + +StripePayFailedModel stripePayFailedModelFromJson(String str) => StripePayFailedModel.fromJson(json.decode(str)); + +String stripePayFailedModelToJson(StripePayFailedModel data) => json.encode(data.toJson()); + +class StripePayFailedModel { + StripePayFailedModel({ + required this.error, + }); + + Error error; + + factory StripePayFailedModel.fromJson(Map json) => StripePayFailedModel( + error: Error.fromJson(json["error"]), + ); + + Map toJson() => { + "error": error.toJson(), + }; +} + +class Error { + Error({ + required this.code, + required this.localizedMessage, + required this.message, + required this.stripeErrorCode, + required this.declineCode, + required this.type, + }); + + String code; + String localizedMessage; + String message; + dynamic stripeErrorCode; + dynamic declineCode; + dynamic type; + + factory Error.fromJson(Map json) => Error( + code: json["code"], + localizedMessage: json["localizedMessage"], + message: json["message"], + stripeErrorCode: json["stripeErrorCode"], + declineCode: json["declineCode"], + type: json["type"], + ); + + Map toJson() => { + "code": code, + "localizedMessage": localizedMessage, + "message": message, + "stripeErrorCode": stripeErrorCode, + "declineCode": declineCode, + "type": type, + }; +} diff --git a/lib/payment/xenditModel.dart b/lib/payment/xenditModel.dart new file mode 100644 index 0000000..d84122e --- /dev/null +++ b/lib/payment/xenditModel.dart @@ -0,0 +1,257 @@ +class XenditModel { + String? id; + String? externalId; + String? userId; + String? status; + String? merchantName; + String? merchantProfilePictureUrl; + int? amount; + String? payerEmail; + String? description; + String? expiryDate; + String? invoiceUrl; + List? availableBanks; + List? availableRetailOutlets; + List? availableEwallets; + List? availableQrCodes; + List? availableDirectDebits; + List? availablePaylaters; + bool? shouldExcludeCreditCard; + bool? shouldSendEmail; + String? created; + String? updated; + String? currency; + Null metadata; + + XenditModel( + {this.id, + this.externalId, + this.userId, + this.status, + this.merchantName, + this.merchantProfilePictureUrl, + this.amount, + this.payerEmail, + this.description, + this.expiryDate, + this.invoiceUrl, + this.availableBanks, + this.availableRetailOutlets, + this.availableEwallets, + this.availableQrCodes, + this.availableDirectDebits, + this.availablePaylaters, + this.shouldExcludeCreditCard, + this.shouldSendEmail, + this.created, + this.updated, + this.currency, + this.metadata}); + + XenditModel.fromJson(Map json) { + id = json['id']; + externalId = json['external_id']; + userId = json['user_id']; + status = json['status']; + merchantName = json['merchant_name']; + merchantProfilePictureUrl = json['merchant_profile_picture_url']; + amount = json['amount']; + payerEmail = json['payer_email']; + description = json['description']; + expiryDate = json['expiry_date']; + invoiceUrl = json['invoice_url']; + if (json['available_banks'] != null) { + availableBanks = []; + json['available_banks'].forEach((v) { + availableBanks!.add(AvailableBanks.fromJson(v)); + }); + } + if (json['available_retail_outlets'] != null) { + availableRetailOutlets = []; + json['available_retail_outlets'].forEach((v) { + availableRetailOutlets!.add(AvailableRetailOutlets.fromJson(v)); + }); + } + if (json['available_ewallets'] != null) { + availableEwallets = []; + json['available_ewallets'].forEach((v) { + availableEwallets!.add(AvailableEwallets.fromJson(v)); + }); + } + if (json['available_qr_codes'] != null) { + availableQrCodes = []; + json['available_qr_codes'].forEach((v) { + availableQrCodes!.add(AvailableQrCodes.fromJson(v)); + }); + } + if (json['available_direct_debits'] != null) { + availableDirectDebits = []; + json['available_direct_debits'].forEach((v) { + availableDirectDebits!.add(AvailableDirectDebits.fromJson(v)); + }); + } + if (json['available_paylaters'] != null) { + availablePaylaters = []; + json['available_paylaters'].forEach((v) { + availablePaylaters!.add(AvailablePaylaters.fromJson(v)); + }); + } + shouldExcludeCreditCard = json['should_exclude_credit_card']; + shouldSendEmail = json['should_send_email']; + created = json['created']; + updated = json['updated']; + currency = json['currency']; + metadata = json['metadata']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['external_id'] = externalId; + data['user_id'] = userId; + data['status'] = status; + data['merchant_name'] = merchantName; + data['merchant_profile_picture_url'] = merchantProfilePictureUrl; + data['amount'] = amount; + data['payer_email'] = payerEmail; + data['description'] = description; + data['expiry_date'] = expiryDate; + data['invoice_url'] = invoiceUrl; + if (availableBanks != null) { + data['available_banks'] = availableBanks!.map((v) => v.toJson()).toList(); + } + if (availableRetailOutlets != null) { + data['available_retail_outlets'] = availableRetailOutlets!.map((v) => v.toJson()).toList(); + } + if (availableEwallets != null) { + data['available_ewallets'] = availableEwallets!.map((v) => v.toJson()).toList(); + } + if (availableQrCodes != null) { + data['available_qr_codes'] = availableQrCodes!.map((v) => v.toJson()).toList(); + } + if (availableDirectDebits != null) { + data['available_direct_debits'] = availableDirectDebits!.map((v) => v.toJson()).toList(); + } + if (availablePaylaters != null) { + data['available_paylaters'] = availablePaylaters!.map((v) => v.toJson()).toList(); + } + data['should_exclude_credit_card'] = shouldExcludeCreditCard; + data['should_send_email'] = shouldSendEmail; + data['created'] = created; + data['updated'] = updated; + data['currency'] = currency; + data['metadata'] = metadata; + return data; + } +} + +class AvailableBanks { + String? bankCode; + String? collectionType; + int? transferAmount; + String? bankBranch; + String? accountHolderName; + int? identityAmount; + + AvailableBanks({this.bankCode, this.collectionType, this.transferAmount, this.bankBranch, this.accountHolderName, this.identityAmount}); + + AvailableBanks.fromJson(Map json) { + bankCode = json['bank_code']; + collectionType = json['collection_type']; + transferAmount = json['transfer_amount']; + bankBranch = json['bank_branch']; + accountHolderName = json['account_holder_name']; + identityAmount = json['identity_amount']; + } + + Map toJson() { + final Map data = {}; + data['bank_code'] = bankCode; + data['collection_type'] = collectionType; + data['transfer_amount'] = transferAmount; + data['bank_branch'] = bankBranch; + data['account_holder_name'] = accountHolderName; + data['identity_amount'] = identityAmount; + return data; + } +} + +class AvailableRetailOutlets { + String? retailOutletName; + + AvailableRetailOutlets({this.retailOutletName}); + + AvailableRetailOutlets.fromJson(Map json) { + retailOutletName = json['retail_outlet_name']; + } + + Map toJson() { + final Map data = {}; + data['retail_outlet_name'] = retailOutletName; + return data; + } +} + +class AvailableEwallets { + String? ewalletType; + + AvailableEwallets({this.ewalletType}); + + AvailableEwallets.fromJson(Map json) { + ewalletType = json['ewallet_type']; + } + + Map toJson() { + final Map data = {}; + data['ewallet_type'] = ewalletType; + return data; + } +} + +class AvailableQrCodes { + String? qrCodeType; + + AvailableQrCodes({this.qrCodeType}); + + AvailableQrCodes.fromJson(Map json) { + qrCodeType = json['qr_code_type']; + } + + Map toJson() { + final Map data = {}; + data['qr_code_type'] = qrCodeType; + return data; + } +} + +class AvailableDirectDebits { + String? directDebitType; + + AvailableDirectDebits({this.directDebitType}); + + AvailableDirectDebits.fromJson(Map json) { + directDebitType = json['direct_debit_type']; + } + + Map toJson() { + final Map data = {}; + data['direct_debit_type'] = directDebitType; + return data; + } +} + +class AvailablePaylaters { + String? paylaterType; + + AvailablePaylaters({this.paylaterType}); + + AvailablePaylaters.fromJson(Map json) { + paylaterType = json['paylater_type']; + } + + Map toJson() { + final Map data = {}; + data['paylater_type'] = paylaterType; + return data; + } +} diff --git a/lib/payment/xenditScreen.dart b/lib/payment/xenditScreen.dart new file mode 100644 index 0000000..a494ab7 --- /dev/null +++ b/lib/payment/xenditScreen.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:customer/payment/xenditModel.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:webview_flutter/webview_flutter.dart'; + +class XenditScreen extends StatefulWidget { + final String initialURl; + final String transId; + final String apiKey; + + const XenditScreen({super.key, required this.initialURl, required this.transId, required this.apiKey}); + + @override + State createState() => _XenditScreenState(); +} + +class _XenditScreenState extends State { + WebViewController controller = WebViewController(); + bool isLoading = true; + + @override + void initState() { + controller.clearCache(); + initController(); + callTransaction(); + super.initState(); + } + + void callTransaction() { + Timer? timer; + timer = Timer.periodic(const Duration(seconds: 4), (Timer t) async { + if (!mounted) { + timer?.cancel(); + return; + } + await Future.delayed(const Duration(seconds: 5)).then((v) async { + final value = await checkStatus(paymentId: widget.transId); + if (!mounted) { + timer?.cancel(); + return; + } + if (value.status == 'PAID' || value.status == 'SETTLED') { + timer?.cancel(); + + Get.back(result: true); + } else if (value.status == 'FAILED') { + timer?.cancel(); + Get.back(result: false); + } + }); + }); + } + + @override + void dispose() { + super.dispose(); + } + + void initController() { + controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: ((url) { + setState(() { + isLoading = false; + }); + }), + onNavigationRequest: (NavigationRequest navigation) async { + log("URL :: ${navigation.url}"); + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(widget.initialURl)); + } + + @override + Widget build(BuildContext context) { + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () async { + _showMyDialog(); + return false; + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + centerTitle: false, + leading: GestureDetector( + onTap: () { + _showMyDialog(); + }, + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + ), + body: Stack(alignment: Alignment.center, children: [WebViewWidget(controller: controller), Visibility(visible: isLoading, child: const Center(child: CircularProgressIndicator()))]), + ), + ); + } + + Future _showMyDialog() async { + return showDialog( + context: context, + barrierDismissible: true, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: Text('Cancel Payment'.tr), + content: SingleChildScrollView(child: Text("Cancel Payment?".tr)), + actions: [ + TextButton( + child: Text('Cancel'.tr, style: const TextStyle(color: Colors.red)), + onPressed: () { + Navigator.of(context).pop(false); + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: Text('Continue'.tr, style: const TextStyle(color: Colors.green)), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ], + ); + }, + ); + } + + Future checkStatus({required String paymentId}) async { + // API endpoint + var url = Uri.parse('https://api.xendit.co/v2/invoices/$paymentId'); + + // Headers + var headers = {'Content-Type': 'application/json', 'Authorization': generateBasicAuthHeader(widget.apiKey.toString())}; + + // Making the POST request + var response = await http.get(url, headers: headers); + + // Checking the response status + if (response.statusCode == 200) { + XenditModel model = XenditModel.fromJson(jsonDecode(response.body)); + return model; + } else { + return XenditModel(); + } + } + + String generateBasicAuthHeader(String apiKey) { + String credentials = '$apiKey:'; + String base64Encoded = base64Encode(utf8.encode(credentials)); + return 'Basic $base64Encoded'; + } +} diff --git a/lib/screen_ui/auth_screens/forgot_password_screen.dart b/lib/screen_ui/auth_screens/forgot_password_screen.dart new file mode 100644 index 0000000..dd3e5de --- /dev/null +++ b/lib/screen_ui/auth_screens/forgot_password_screen.dart @@ -0,0 +1,98 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/forgot_password_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import 'login_screen.dart'; + +class ForgotPasswordScreen extends StatelessWidget { + const ForgotPasswordScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetX( + init: ForgotPasswordController(), + builder: (controller) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + elevation: 0, + actions: [ + TextButton( + onPressed: () {}, + style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 40), tapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Skip".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + Padding(padding: const EdgeInsets.only(top: 2), child: Icon(Icons.arrow_forward_ios, size: 16, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + ], + ), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 15, right: 15, bottom: 20), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter your registered email to receive a reset link.".tr, + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 24), + TextFieldWidget(title: "Email Address*".tr, hintText: "jerome014@gmail.com", controller: controller.emailEditingController.value), + const SizedBox(height: 30), + RoundedButtonFill( + title: "Send Link".tr, + onPress: controller.forgotPassword, + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + textColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Center( + child: Text.rich( + TextSpan( + text: "Remember Password?".tr, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + children: [ + TextSpan( + text: "Log in".tr, + style: AppThemeData.mediumTextStyle(color: AppThemeData.ecommerce300, decoration: TextDecoration.underline, decorationColor: AppThemeData.ecommerce300), + recognizer: + TapGestureRecognizer() + ..onTap = () { + Get.offAll(() => const LoginScreen()); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/auth_screens/login_screen.dart b/lib/screen_ui/auth_screens/login_screen.dart new file mode 100644 index 0000000..1b311cc --- /dev/null +++ b/lib/screen_ui/auth_screens/login_screen.dart @@ -0,0 +1,186 @@ +import 'dart:io'; + +import 'package:customer/screen_ui/auth_screens/sign_up_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import '../../controllers/login_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import 'package:get/get.dart'; +import 'forgot_password_screen.dart'; +import 'mobile_login_screen.dart'; + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetX( + init: LoginController(), + builder: (controller) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + TextButton( + onPressed: () { + Get.to(() => LocationPermissionScreen()); + }, + child: Row( + children: [ + Text("Skip".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Icon(Icons.arrow_forward_ios, size: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ], + ), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 15, right: 15, bottom: 15), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Log in to explore your all in one vendor app favourites and shop effortlessly.".tr, + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 24), + TextFieldWidget(title: "Email Address*".tr, hintText: "jerome014@gmail.com", controller: controller.emailController.value, focusNode: controller.emailFocusNode), + const SizedBox(height: 15), + TextFieldWidget( + title: "Password*".tr, + hintText: "Enter password".tr, + controller: controller.passwordController.value, + obscureText: controller.passwordVisible.value, + focusNode: controller.passwordFocusNode, + suffix: Padding( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () { + controller.passwordVisible.value = !controller.passwordVisible.value; + }, + child: + controller.passwordVisible.value + ? SvgPicture.asset("assets/icons/ic_password_show.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn)) + : SvgPicture.asset("assets/icons/ic_password_close.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn)), + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => Get.to(() => const ForgotPasswordScreen()), + child: Text("Forgot Password".tr, style: AppThemeData.semiBoldTextStyle(color: AppThemeData.info400)), + ), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + onPress: controller.loginWithEmail, + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + textColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + ), + const SizedBox(height: 25), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container(width: 52, height: 1, color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey300), + const SizedBox(width: 15), + Text("or continue with".tr, style: AppThemeData.regularTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900.withOpacity(0.6))), + const SizedBox(width: 15), + Container(width: 52, height: 1, color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey300), + ], + ), + const SizedBox(height: 25), + RoundedButtonFill( + title: "Mobile number".tr, + onPress: () => Get.to(() => const MobileLoginScreen()), + isRight: false, + isCenter: true, + icon: Icon(Icons.mobile_friendly_outlined, size: 20, color: isDark ? AppThemeData.greyDark900 : null), + //Image.asset(AppAssets.icMessage, width: 20, height: 18, color: isDark ? AppThemeData.greyDark900 : null), + color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey300, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: RoundedButtonFill( + title: "with Google".tr, + textColor: isDark ? AppThemeData.grey100 : AppThemeData.grey900, + color: isDark ? AppThemeData.grey900 : AppThemeData.grey100, + icon: SvgPicture.asset("assets/icons/ic_google.svg"), + isRight: false, + isCenter: true, + onPress: () async { + controller.loginWithGoogle(); + }, + ), + ), + const SizedBox(width: 10), + Platform.isIOS + ? Expanded( + child: RoundedButtonFill( + title: "with Apple".tr, + isCenter: true, + textColor: isDark ? AppThemeData.grey100 : AppThemeData.grey900, + color: isDark ? AppThemeData.grey900 : AppThemeData.grey100, + icon: SvgPicture.asset("assets/icons/ic_apple.svg"), + isRight: false, + onPress: () async { + controller.loginWithApple(); + }, + ), + ) + : const SizedBox(), + ], + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Center( + child: Text.rich( + TextSpan( + text: "Didn't have an account?".tr, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + children: [ + TextSpan( + text: "Sign up".tr, + style: AppThemeData.mediumTextStyle(color: AppThemeData.ecommerce300, decoration: TextDecoration.underline), + recognizer: + TapGestureRecognizer() + ..onTap = () { + Get.offAll(() => const SignUpScreen()); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/auth_screens/mobile_login_screen.dart b/lib/screen_ui/auth_screens/mobile_login_screen.dart new file mode 100644 index 0000000..d5dd719 --- /dev/null +++ b/lib/screen_ui/auth_screens/mobile_login_screen.dart @@ -0,0 +1,166 @@ +import 'package:country_code_picker/country_code_picker.dart'; +import 'package:customer/screen_ui/auth_screens/sign_up_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import '../../constant/assets.dart'; +import '../../constant/constant.dart'; +import '../../controllers/mobile_login_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; + +class MobileLoginScreen extends StatelessWidget { + const MobileLoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetX( + init: MobileLoginController(), + builder: (controller) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + leading: IconButton( + icon: Icon(Icons.arrow_back, size: 20, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + onPressed: () { + Get.back(); + }, + ), + actions: [ + TextButton( + onPressed: () { + Get.to(() => LocationPermissionScreen()); + }, + style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 40), tapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Skip".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + Padding(padding: const EdgeInsets.only(top: 2, left: 4), child: Icon(Icons.arrow_forward_ios, size: 16, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + ], + ), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 16), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use your mobile number to Log in easily and securely.".tr, + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 25), + TextFieldWidget( + title: "Mobile Number*".tr, + hintText: "Enter Mobile number".tr, + controller: controller.mobileController.value, + textInputType: const TextInputType.numberWithOptions(signed: true, decimal: true), + textInputAction: TextInputAction.done, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9]')), LengthLimitingTextInputFormatter(10)], + prefix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CountryCodePicker( + onChanged: (value) { + controller.countryCodeController.value.text = value.dialCode ?? Constant.defaultCountryCode; + }, + initialSelection: controller.countryCodeController.value.text.isNotEmpty ? controller.countryCodeController.value.text : Constant.defaultCountryCode, + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + textStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : Colors.black), + dialogTextStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + searchStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + dialogBackgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + padding: EdgeInsets.zero, + ), + // const Icon(Icons.keyboard_arrow_down_rounded, size: 24, color: AppThemeData.grey400), + Container(height: 24, width: 1, color: AppThemeData.grey400), + const SizedBox(width: 4), + ], + ), + ), + const SizedBox(height: 30), + RoundedButtonFill( + title: "Send Code".tr, + onPress: controller.sendOtp, + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + textColor: isDark ? AppThemeData.surfaceDark : Colors.white, + ), + const SizedBox(height: 25), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container(width: 52, height: 1, color: isDark ? AppThemeData.greyDark300 : AppThemeData.grey300), + const SizedBox(width: 15), + Text("or continue with".tr, style: AppThemeData.regularTextStyle(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400)), + const SizedBox(width: 15), + Container(width: 52, height: 1, color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400), + ], + ), + const SizedBox(height: 25), + RoundedButtonFill( + title: "Email address".tr, + onPress: () => Get.to(() => const SignUpScreen()), + isRight: false, + isCenter: true, + icon: Image.asset(AppAssets.icMessage, width: 20, height: 18, color: isDark ? AppThemeData.greyDark900 : null), + color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Center( + child: Text.rich( + TextSpan( + text: "Didn't have an account?".tr, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + children: [ + TextSpan( + text: "Sign up".tr, + style: AppThemeData.mediumTextStyle( + color: AppThemeData.ecommerce300, + decoration: TextDecoration.underline, + decorationColor: AppThemeData.ecommerce300, + decorationStyle: TextDecorationStyle.solid, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + Get.offAll(() => const SignUpScreen()); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/auth_screens/otp_verification_screen.dart b/lib/screen_ui/auth_screens/otp_verification_screen.dart new file mode 100644 index 0000000..07857fc --- /dev/null +++ b/lib/screen_ui/auth_screens/otp_verification_screen.dart @@ -0,0 +1,154 @@ +import 'package:customer/screen_ui/auth_screens/sign_up_screen.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; +import '../../constant/assets.dart'; +import '../../controllers/otp_verification_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; + +class OtpVerificationScreen extends StatelessWidget { + const OtpVerificationScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetX( + init: OtpVerifyController(), + builder: (controller) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + leading: IconButton( + icon: Icon(Icons.arrow_back, size: 20, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + onPressed: () { + Get.back(); + }, + ), + actions: [ + TextButton( + onPressed: () { + // Handle skip action + }, + style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 40), tapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Skip".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + Padding( + padding: const EdgeInsets.only(top: 2, left: 4), + child: Icon(Icons.arrow_forward_ios, size: 16, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + ], + ), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 16), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${"Enter the OTP sent to your mobile".tr} ${controller.countryCode} ${controller.maskPhoneNumber(controller.phoneNumber.value)}", + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + + const SizedBox(height: 30), + + /// OTP Field + PinCodeTextField( + appContext: context, + length: 6, + controller: controller.otpController.value, + keyboardType: TextInputType.number, + cursorColor: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500, + enablePinAutofill: true, + hintCharacter: "-", + textStyle: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + pinTheme: PinTheme( + shape: PinCodeFieldShape.box, + borderRadius: BorderRadius.circular(12), + fieldHeight: 54, + fieldWidth: 51, + inactiveColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + inactiveFillColor: Colors.transparent, + selectedColor: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400, + selectedFillColor: isDark ? AppThemeData.surfaceDark : AppThemeData.grey50, + activeColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + activeFillColor: Colors.transparent, + errorBorderColor: AppThemeData.danger300, + disabledColor: Colors.transparent, + borderWidth: 1, + ), + enableActiveFill: true, + onCompleted: (v) {}, + onChanged: (value) {}, + ), + + /// Resend OTP + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(AppAssets.icArrowsClockwise, height: 20, width: 20), + TextButton( + onPressed: () { + controller.otpController.value.clear(); + controller.sendOTP(); + }, + child: Text("Resend OTP".tr, style: AppThemeData.semiBoldTextStyle(color: AppThemeData.info400, fontSize: 16)), + ), + ], + ), + + const SizedBox(height: 10), + + /// Verify Button + RoundedButtonFill( + title: "Verify".tr, + onPress: controller.verifyOtp, + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + textColor: isDark ? AppThemeData.surfaceDark : Colors.white, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Center( + child: Text.rich( + TextSpan( + text: "Didn't have an account?".tr, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + children: [ + TextSpan( + text: "Sign up".tr, + style: AppThemeData.mediumTextStyle(color: AppThemeData.ecommerce300, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () => Get.offAll(() => const SignUpScreen()), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/auth_screens/sign_up_screen.dart b/lib/screen_ui/auth_screens/sign_up_screen.dart new file mode 100644 index 0000000..69c9645 --- /dev/null +++ b/lib/screen_ui/auth_screens/sign_up_screen.dart @@ -0,0 +1,230 @@ +import 'package:country_code_picker/country_code_picker.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import '../../constant/constant.dart'; +import '../../controllers/sign_up_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import 'package:get/get.dart'; +import 'login_screen.dart'; +import 'mobile_login_screen.dart'; + +class SignUpScreen extends StatelessWidget { + const SignUpScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetX( + init: SignUpController(), + builder: (controller) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + elevation: 0, + actions: [ + TextButton( + onPressed: () { + Get.to(() => LocationPermissionScreen()); + }, + style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 40), tapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Skip".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Icon(Icons.arrow_forward_ios, size: 16, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + ], + ), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 15, right: 15, top: 10), + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sign up to explore all our services and start shopping, riding, and more.".tr, + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: TextFieldWidget(title: "First Name*".tr, hintText: "Jerome".tr, controller: controller.firstNameController.value)), + const SizedBox(width: 10), + Expanded(child: TextFieldWidget(title: "Last Name*".tr, hintText: "Bell".tr, controller: controller.lastNameController.value)), + ], + ), + const SizedBox(height: 15), + TextFieldWidget( + title: "Email Address*".tr, + hintText: "jerome014@gmail.com", + controller: controller.emailController.value, + focusNode: controller.emailFocusNode, + ), + const SizedBox(height: 15), + TextFieldWidget( + title: "Mobile Number*".tr, + hintText: "Enter Mobile number".tr, + enable: controller.type.value == "mobileNumber" ? false : true, + controller: controller.mobileController.value, + textInputType: const TextInputType.numberWithOptions(signed: true, decimal: true), + textInputAction: TextInputAction.done, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9]')), LengthLimitingTextInputFormatter(10)], + prefix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CountryCodePicker( + onChanged: (value) { + controller.countryCodeController.value.text = value.dialCode ?? Constant.defaultCountryCode; + }, + initialSelection: controller.countryCodeController.value.text.isNotEmpty ? controller.countryCodeController.value.text : Constant.defaultCountryCode, + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + enabled: controller.type.value != "mobileNumber", + textStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : Colors.black), + dialogTextStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + searchStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + dialogBackgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + padding: EdgeInsets.zero, + ), + // const Icon(Icons.keyboard_arrow_down_rounded, size: 24, color: AppThemeData.grey400), + Container(height: 24, width: 1, color: AppThemeData.grey400), + const SizedBox(width: 4), + ], + ), + ), + const SizedBox(height: 15), + TextFieldWidget( + title: "Password*".tr, + hintText: "Enter password".tr, + controller: controller.passwordController.value, + obscureText: controller.passwordVisible.value, + focusNode: controller.passwordFocusNode, + suffix: Padding( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () { + controller.passwordVisible.value = !controller.passwordVisible.value; + }, + child: + controller.passwordVisible.value + ? SvgPicture.asset( + "assets/icons/ic_password_show.svg", + colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn), + ) + : SvgPicture.asset( + "assets/icons/ic_password_close.svg", + colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn), + ), + ), + ), + ), + const SizedBox(height: 15), + TextFieldWidget( + title: "Confirm Password*".tr, + hintText: "Enter confirm password".tr, + controller: controller.confirmPasswordController.value, + obscureText: controller.conformPasswordVisible.value, + suffix: Padding( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () { + controller.conformPasswordVisible.value = !controller.conformPasswordVisible.value; + }, + child: + controller.conformPasswordVisible.value + ? SvgPicture.asset( + "assets/icons/ic_password_show.svg", + colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn), + ) + : SvgPicture.asset( + "assets/icons/ic_password_close.svg", + colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn), + ), + ), + ), + ), + const SizedBox(height: 15), + TextFieldWidget(title: "Referral Code".tr, hintText: "Enter referral code".tr, controller: controller.referralController.value), + const SizedBox(height: 40), + RoundedButtonFill( + title: "Sign up".tr, + onPress: () => controller.signUp(), + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + textColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + ), + const SizedBox(height: 25), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container(width: 52, height: 1, color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey300), + const SizedBox(width: 15), + Text("or continue with".tr, style: AppThemeData.regularTextStyle(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400)), + const SizedBox(width: 15), + Container(width: 52, height: 1, color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey300), + ], + ), + const SizedBox(height: 25), + RoundedButtonFill( + title: "Mobile number".tr, + onPress: () => Get.to(() => const MobileLoginScreen()), + isRight: false, + isCenter: true, + icon: Icon(Icons.mobile_friendly_outlined, size: 20, color: isDark ? AppThemeData.greyDark900 : null), + //Image.asset(AppAssets.icMessage, width: 20, height: 18, color: isDark ? AppThemeData.greyDark900 : null), + color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + const SizedBox(height: 25), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Center( + child: Text.rich( + TextSpan( + text: "Already have an account?".tr, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + children: [ + TextSpan( + text: "Log in".tr, + style: AppThemeData.mediumTextStyle( + color: AppThemeData.ecommerce300, + decoration: TextDecoration.underline, + decorationColor: AppThemeData.ecommerce300, + decorationStyle: TextDecorationStyle.solid, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + Get.offAll(() => const LoginScreen()); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/Intercity_home_screen.dart b/lib/screen_ui/cab_service_screens/Intercity_home_screen.dart new file mode 100644 index 0000000..2095b2f --- /dev/null +++ b/lib/screen_ui/cab_service_screens/Intercity_home_screen.dart @@ -0,0 +1,1586 @@ +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/controllers/Intercity_home_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/vehicle_type.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/screen_ui/cab_service_screens/cab_coupon_code_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_border.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/utils/utils.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:geocoding/geocoding.dart' as get_cord_address; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:latlong2/latlong.dart' as latlong; +import 'package:location/location.dart'; +import '../../constant/constant.dart'; +import '../../controllers/cab_dashboard_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/user_model.dart'; +import '../../service/fire_store_utils.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../themes/text_field_widget.dart'; +import '../../widget/osm_map/map_picker_page.dart'; +import '../../widget/place_picker/location_picker_screen.dart'; +import '../../widget/place_picker/selected_location_model.dart'; + +class IntercityHomeScreen extends StatelessWidget { + const IntercityHomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: IntercityHomeController(), + builder: (controller) { + return Scaffold( + body: + controller.isLoading.value + ? Constant.loader() + : Stack( + children: [ + Constant.selectedMapType == "osm" + ? flutterMap.FlutterMap( + mapController: controller.mapOsmController, + options: flutterMap.MapOptions( + initialCenter: + Constant.currentLocation != null + ? latlong.LatLng(Constant.currentLocation!.latitude, Constant.currentLocation!.longitude) + : controller.currentOrder.value.id != null + ? latlong.LatLng( + double.parse(controller.currentOrder.value.sourceLocation!.latitude.toString()), + double.parse(controller.currentOrder.value.sourceLocation!.longitude.toString()), + ) + : latlong.LatLng(41.4219057, -102.0840772), + initialZoom: 14, + ), + children: [ + flutterMap.TileLayer(urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: Platform.isAndroid ? "com.emart" : "com.emart.ios"), + flutterMap.MarkerLayer(markers: controller.osmMarker), + if (controller.routePoints.isNotEmpty) flutterMap.PolylineLayer(polylines: [flutterMap.Polyline(points: controller.routePoints, strokeWidth: 5.0, color: Colors.blue)]), + ], + ) + : GoogleMap( + onMapCreated: (googleMapController) { + controller.mapController = googleMapController; + + if (Constant.currentLocation != null) { + controller.setDepartureMarker(Constant.currentLocation!.latitude, Constant.currentLocation!.longitude); + controller.searchPlaceNameGoogle(); + } + }, + initialCameraPosition: CameraPosition(target: controller.currentPosition.value, zoom: 14), + myLocationEnabled: true, + zoomControlsEnabled: true, + zoomGesturesEnabled: true, + polylines: Set.of(controller.polyLines.values), + markers: controller.markers.toSet(), // reactive marker set + ), + Positioned( + top: 50, + left: Constant.isRtl ? null : 20, + right: Constant.isRtl ? 20 : null, + child: InkWell( + onTap: () { + if (controller.bottomSheetType.value == "vehicleSelection") { + controller.bottomSheetType.value = "location"; + } else if (controller.bottomSheetType.value == "payment") { + controller.bottomSheetType.value = "vehicleSelection"; + } else if (controller.bottomSheetType.value == "conformRide") { + controller.bottomSheetType.value = "payment"; + } else if (controller.bottomSheetType.value == "waitingDriver" || controller.bottomSheetType.value == "driverDetails") { + Get.back(result: true); + } else { + Get.back(); + } + }, + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.all(10), + child: Center(child: Icon(Icons.arrow_back_ios_new, size: 20, color: isDark ? AppThemeData.grey50 : AppThemeData.greyDark50)), + ), + ), + ), + ), + controller.bottomSheetType.value == "location" + ? searchLocationBottomSheet(context, controller, isDark) + : controller.bottomSheetType.value == "vehicleSelection" + ? vehicleSelection(context, controller, isDark) + : controller.bottomSheetType.value == "payment" + ? paymentBottomSheet(context, controller, isDark) + : controller.bottomSheetType.value == "conformRide" + ? conformBottomSheet(context, isDark) + : controller.bottomSheetType.value == "waitingForDriver" + ? waitingDialog(context, controller, isDark) + : controller.bottomSheetType.value == "driverDetails" + ? driverDialog(context, controller, isDark) + : SizedBox(), + ], + ), + ); + }, + ); + } + + Widget searchLocationBottomSheet(BuildContext context, IntercityHomeController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.48, + minChildSize: 0.48, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(35))), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + SizedBox(height: 10), + Stack( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Pickup Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + controller.sourceTextEditController.value.text = ''; + final firstPlace = result; + if (result != null) { + if (Constant.checkZoneCheck(firstPlace.coordinates.latitude, firstPlace.coordinates.longitude) == true) { + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.sourceTextEditController.value.text = address.toString(); + controller.setDepartureMarker(lat, lng); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + if (Constant.checkZoneCheck(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude) == true) { + controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDepartureMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + }); + } + }, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Pickup Location".tr, + enable: false, + prefix: Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Image.asset("assets/icons/pickup.png", height: 22, width: 22)), + ), + ), + const SizedBox(height: 10), + // Destination Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.destinationTextEditController.value.text = ''; + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.destinationTextEditController.value.text = address.toString(); + controller.setDestinationMarker(lat, lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.destinationTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDestinationMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } + }); + } + }, + child: TextFieldWidget( + controller: controller.destinationTextEditController.value, + // backgroundColor: AppThemeData.grey50, + // borderColor: AppThemeData.grey50, + hintText: "Destination Location".tr, + enable: false, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.radio_button_checked, color: Colors.red)), + ), + ), + ], + ), + ), + Positioned( + left: 10, + top: 33, + child: DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 40), + ), + ), + ], + ), + SizedBox(height: 15), + Align(alignment: Alignment.centerLeft, child: Text("Popular Destinations".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: AppThemeData.grey900))), + SizedBox( + height: 120, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView.builder( + itemCount: controller.popularDestination.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return InkWell( + onTap: () async { + if (controller.popularDestination[index].latitude != null || controller.popularDestination[index].longitude != null) { + List placeMarks = await get_cord_address.placemarkFromCoordinates( + controller.popularDestination[index].latitude ?? 0.0, + controller.popularDestination[index].longitude ?? 0.0, + ); + + final address = + (placeMarks.first.subLocality!.isEmpty ? '' : "${placeMarks.first.subLocality}, ") + + (placeMarks.first.street!.isEmpty ? '' : "${placeMarks.first.street}, ") + + (placeMarks.first.name!.isEmpty ? '' : "${placeMarks.first.name}, ") + + (placeMarks.first.subAdministrativeArea!.isEmpty ? '' : "${placeMarks.first.subAdministrativeArea}, ") + + (placeMarks.first.administrativeArea!.isEmpty ? '' : "${placeMarks.first.administrativeArea}, ") + + (placeMarks.first.country!.isEmpty ? '' : "${placeMarks.first.country}, ") + + (placeMarks.first.postalCode!.isEmpty ? '' : "${placeMarks.first.postalCode}, "); + controller.destinationTextEditController.value.text = address; + controller.setDestinationMarker(controller.popularDestination[index].latitude ?? 0.0, controller.popularDestination[index].longitude ?? 0.0); + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: Stack( + children: [ + CachedNetworkImage( + imageUrl: + (controller.popularDestination[index].image != null && controller.popularDestination[index].image!.isNotEmpty) + ? controller.popularDestination[index].image! + : Constant.placeHolderImage, + height: 160, + width: 120, + imageBuilder: + (context, imageProvider) => + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: + (context, url, error) => + ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(Constant.placeHolderImage, fit: BoxFit.cover, cacheHeight: 80, cacheWidth: 80)), + fit: BoxFit.cover, + ), + Positioned( + left: 5, + top: 80, + child: Text(controller.popularDestination[index].title.toString(), style: AppThemeData.boldTextStyle(fontSize: 15, color: AppThemeData.surface)), + ), + ], + ), + ), + ); + }, + ), + ), + ), + SizedBox(height: 10), + RoundedButtonFill( + title: "Continue".tr, + onPress: () { + if (controller.sourceTextEditController.value.text.isEmpty) { + ShowToastDialog.showToast("Please select source location".tr); + } else if (controller.destinationTextEditController.value.text.isEmpty) { + ShowToastDialog.showToast("Please select destination location".tr); + } else { + controller.bottomSheetType.value = "vehicleSelection"; + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ); + }, + ), + ); + } + + Widget vehicleSelection(BuildContext context, IntercityHomeController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.40, + minChildSize: 0.40, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: const BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + "Select Your Vehicle Type".tr, + style: AppThemeData.boldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + textAlign: TextAlign.start, + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: controller.vehicleTypes.length, + shrinkWrap: true, + padding: EdgeInsets.only(bottom: 20), + controller: scrollController, + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + VehicleType vehicleType = controller.vehicleTypes[index]; + return Obx( + () => InkWell( + onTap: () { + controller.selectedVehicleType.value = controller.vehicleTypes[index]; + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: + isDark + ? controller.selectedVehicleType.value.id == vehicleType.id + ? Colors.white + : AppThemeData.grey500 + : controller.selectedVehicleType.value.id == vehicleType.id + ? AppThemeData.grey300 + : Colors.transparent, + width: 1, + ), + color: + controller.selectedVehicleType.value.id == vehicleType.id + ? AppThemeData.grey50 + : isDark + ? AppThemeData.grey300 + : Colors.white, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Row( + children: [ + ClipRRect( + //borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: vehicleType.vehicleIcon.toString(), + height: 60, + width: 60, + imageBuilder: + (context, imageProvider) => + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => ClipRRect(borderRadius: BorderRadius.circular(20), child: Image.network(Constant.userPlaceHolder, fit: BoxFit.cover)), + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${vehicleType.name} | ${controller.distance.toStringAsFixed(2)}km", + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1), + ), + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text(controller.duration.value, style: const TextStyle(fontWeight: FontWeight.w400, letterSpacing: 1)), + ), + ], + ), + ), + ), + Text( + Constant.amountShow(amount: controller.getAmount(vehicleType).toString()), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + Obx( + () => RoundedButtonFill( + title: 'pay_amount'.trParams({ + 'amount': + controller.selectedVehicleType.value.id == null + ? Constant.amountShow(amount: "0.0") + : Constant.amountShow(amount: controller.getAmount(controller.selectedVehicleType.value).toString()), + }), + // title: + // "Pay ${controller.selectedVehicleType.value.id == null ? Constant.amountShow(amount: "0.0") : Constant.amountShow(amount: controller.getAmount(controller.selectedVehicleType.value).toString())}", + onPress: () async { + if (controller.selectedVehicleType.value.id != null) { + controller.calculateTotalAmount(); + controller.bottomSheetType.value = "payment"; + } else { + ShowToastDialog.showToast("Please select a vehicle type first.".tr); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget paymentBottomSheet(BuildContext context, IntercityHomeController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.70, + minChildSize: 0.30, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Select Payment Method".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Icon(Icons.close, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + controller: scrollController, + children: [ + Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + const SizedBox(height: 10), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + onPress: () async { + if (controller.selectedPaymentMethod.value.isEmpty) { + ShowToastDialog.showToast("Please select a payment method".tr); + return; + } + if (controller.selectedPaymentMethod.value == "wallet") { + num walletAmount = controller.userModel.value.walletAmount ?? 0; + if (walletAmount <= 0) { + ShowToastDialog.showToast("Insufficient wallet balance. Please select another payment method.".tr); + return; + } + } + if (controller.currentOrder.value.id != null) { + controller.bottomSheetType.value = "driverDetails"; + } else { + controller.bottomSheetType.value = "conformRide"; + } + }, + ), + // RoundedButtonFill( + // title: "Continue".tr, + // color: AppThemeData.primary300, + // textColor: AppThemeData.grey900, + // onPress: () async { + // if (controller.selectedPaymentMethod.value.isEmpty) { + // ShowToastDialog.showToast("Please select a payment method"); + // } else { + // if (controller.currentOrder.value.id != null) { + // controller.bottomSheetType.value = "driverDetails"; + // } else { + // controller.bottomSheetType.value = "conformRide"; + // } + // } + // }, + // ), + ], + ), + ); + }, + ), + ); + } + + Widget conformBottomSheet(BuildContext context, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.3, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return GetX( + init: IntercityHomeController(), + builder: (controller) { + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + Expanded( + child: ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + const SizedBox(height: 10), + Stack( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? Colors.transparent : Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Pickup Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.sourceTextEditController.value.text = ''; + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.sourceTextEditController.value.text = address.toString(); + controller.setDepartureMarker(lat, lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDepartureMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } + }); + } + }, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Pickup Location".tr, + enable: false, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.stop_circle_outlined, color: Colors.green)), + ), + ), + const SizedBox(height: 10), + // Destination Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.destinationTextEditController.value.text = ''; + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.destinationTextEditController.value.text = address.toString(); + controller.setDestinationMarker(lat, lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.destinationTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDestinationMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } + }); + } + }, + child: TextFieldWidget( + controller: controller.destinationTextEditController.value, + // backgroundColor: AppThemeData.grey50, + // borderColor: AppThemeData.grey50, + hintText: "Destination Location".tr, + enable: false, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.radio_button_checked, color: Colors.red)), + ), + ), + ], + ), + ), + Positioned( + left: 10, + top: 33, + child: DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 40), + ), + ), + ], + ), + const SizedBox(height: 10), + + Row( + children: [ + Expanded(child: Text("Promo code".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + InkWell( + onTap: () { + Get.to(CabCouponCodeScreen())!.then((value) { + if (value != null) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: value); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = value; + controller.calculateTotalAmount(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } + }); + }, + child: Text( + "View All".tr, + style: AppThemeData.boldTextStyle(decoration: TextDecoration.underline, fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 5), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + width: Responsive.width(100, context), + height: Responsive.height(6, context), + color: AppThemeData.carRent50, + child: DottedBorder( + options: RectDottedBorderOptions(dashPattern: [10, 5], strokeWidth: 1, padding: EdgeInsets.all(0), color: AppThemeData.carRent400), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_coupon.svg"), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextFormField( + controller: controller.couponCodeTextEditController.value, + style: AppThemeData.semiBoldTextStyle(color: AppThemeData.parcelService500, fontSize: 16), + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'Write coupon Code'.tr, + contentPadding: EdgeInsets.only(bottom: 10), + hintStyle: AppThemeData.semiBoldTextStyle(color: AppThemeData.parcelService500, fontSize: 16), + ), + ), + ), + ), + RoundedButtonFill( + title: "Redeem now".tr, + width: 27, + borderRadius: 10, + fontSizes: 14, + onPress: () async { + if (controller.cabCouponList + .where((element) => element.code!.toLowerCase() == controller.couponCodeTextEditController.value.text.toLowerCase()) + .isNotEmpty) { + CouponModel couponModel = controller.cabCouponList.firstWhere( + (p0) => p0.code!.toLowerCase() == controller.couponCodeTextEditController.value.text.toLowerCase(), + ); + if (couponModel.expiresAt!.toDate().isAfter(DateTime.now())) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: couponModel); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = couponModel; + controller.calculateTotalAmount(); + controller.update(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } else { + ShowToastDialog.showToast("This coupon code has been expired".tr); + } + } else { + ShowToastDialog.showToast("Invalid coupon code".tr); + } + }, + color: AppThemeData.parcelService300, + textColor: AppThemeData.grey50, + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 10), + + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Subtotal".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + Constant.amountShow(amount: controller.subTotal.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text("Discount".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + SizedBox(width: 5), + Text( + controller.selectedCouponModel.value.id == null ? "" : "(${controller.selectedCouponModel.value.code})", + style: AppThemeData.mediumTextStyle(fontSize: 16, color: AppThemeData.primary300), + ), + ], + ), + Text(Constant.amountShow(amount: controller.discount.value.toString()), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.danger300)), + ], + ), + ), + + // Tax List + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + children: [ + Expanded( + child: Text( + '${taxModel.title} (${taxModel.tax} ${taxModel.type == "Fixed" ? Constant.currencyData!.code : "%"})'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + Constant.amountShow( + amount: Constant.calculateTax(amount: (controller.subTotal.value - controller.discount.value).toString(), taxModel: taxModel).toString(), + ).tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ); + }, + ), + const Divider(), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Order Total".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + Constant.amountShow(amount: controller.totalAmount.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(10), + child: Row( + children: [ + controller.selectedPaymentMethod.value == '' + ? cardDecorationScreen(controller, PaymentGateway.wallet, isDark, "") + : controller.selectedPaymentMethod.value == PaymentGateway.wallet.name + ? cardDecorationScreen(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png") + : controller.selectedPaymentMethod.value == PaymentGateway.cod.name + ? cardDecorationScreen(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png") + : controller.selectedPaymentMethod.value == PaymentGateway.stripe.name + ? cardDecorationScreen(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png") + : controller.selectedPaymentMethod.value == PaymentGateway.paypal.name + ? cardDecorationScreen(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payStack.name + ? cardDecorationScreen(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png") + : controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name + ? cardDecorationScreen(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png") + : controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name + ? cardDecorationScreen(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payFast.name + ? cardDecorationScreen(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png") + : controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name + ? cardDecorationScreen(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png") + : controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name + ? cardDecorationScreen(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png") + : controller.selectedPaymentMethod.value == PaymentGateway.xendit.name + ? cardDecorationScreen(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png") + : cardDecorationScreen(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + SizedBox(width: 22), + Text( + controller.selectedPaymentMethod.value.tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + RoundedButtonFill( + title: "Confirm Booking".tr, + onPress: () async { + controller.placeOrder(); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } + + Widget waitingDialog(BuildContext context, IntercityHomeController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.4, + minChildSize: 0.4, + maxChildSize: 0.4, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + SizedBox(height: 30), + Text("Waiting for driver....".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: AppThemeData.grey900)), + Image.asset('assets/loader.gif', width: 250), + RoundedButtonFill( + title: "Cancel Ride".tr, + onPress: () async { + try { + // 1. Update current order status + controller.currentOrder.update((order) { + if (order != null) { + order.status = Constant.orderRejected; + } + }); + + // 2. Save to Firestore + if (controller.currentOrder.value.id != null) { + await FireStoreUtils.updateCabOrder(controller.currentOrder.value); + } + + // 3. Reset controller states + controller.bottomSheetType.value = ""; + controller.polyLines.clear(); + controller.markers.clear(); + controller.osmMarker.clear(); + controller.routePoints.clear(); + controller.sourceTextEditController.value.clear(); + controller.destinationTextEditController.value.clear(); + controller.departureLatLong.value = const LatLng(0.0, 0.0); + controller.destinationLatLong.value = const LatLng(0.0, 0.0); + controller.departureLatLongOsm.value = latlong.LatLng(0.0, 0.0); + controller.destinationLatLongOsm.value = latlong.LatLng(0.0, 0.0); + + // 4. Reset user’s in-progress order + if (Constant.userModel != null) { + Constant.userModel!.inProgressOrderID = null; + await FireStoreUtils.updateUser(Constant.userModel!); + } + + // 5. Optional feedback + ShowToastDialog.showToast("Ride cancelled successfully".tr); + Get.back(); + CabDashboardController cabDashboardController = Get.put(CabDashboardController()); + cabDashboardController.selectedIndex.value = 0; + } catch (e) { + ShowToastDialog.showToast("Failed to cancel ride".tr); + } + }, + color: AppThemeData.danger300, + textColor: AppThemeData.surface, + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget driverDialog(BuildContext context, IntercityHomeController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.7, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + Expanded( + child: ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + const SizedBox(height: 10), + Stack( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? Colors.transparent : Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Pickup Location + InkWell( + onTap: () async { + // if (Constant.selectedMapType == 'osm') { + // final result = await Get.to(() => MapPickerPage()); + // if (result != null) { + // controller.sourceTextEditController.value.text = ''; + // final firstPlace = result; + // final lat = firstPlace.coordinates.latitude; + // final lng = firstPlace.coordinates.longitude; + // final address = firstPlace.address; + // controller.sourceTextEditController.value.text = address.toString(); + // controller.setDepartureMarker(lat, lng); + // } + // } else { + // Get.to(LocationPickerScreen())!.then((value) async { + // if (value != null) { + // SelectedLocationModel selectedLocationModel = value; + // + // controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + // controller.setDepartureMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + // } + // }); + // } + }, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Pickup Location".tr, + enable: false, + readOnly: true, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.stop_circle_outlined, color: Colors.green)), + ), + ), + const SizedBox(height: 10), + // Destination Location + InkWell( + onTap: () async { + // if (Constant.selectedMapType == 'osm') { + // final result = await Get.to(() => MapPickerPage()); + // if (result != null) { + // controller.destinationTextEditController.value.text = ''; + // final firstPlace = result; + // final lat = firstPlace.coordinates.latitude; + // final lng = firstPlace.coordinates.longitude; + // final address = firstPlace.address; + // controller.destinationTextEditController.value.text = address.toString(); + // controller.setDestinationMarker(lat, lng); + // } + // } else { + // Get.to(LocationPickerScreen())!.then((value) async { + // if (value != null) { + // SelectedLocationModel selectedLocationModel = value; + // + // controller.destinationTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + // controller.setDestinationMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + // } + // }); + // } + }, + child: TextFieldWidget( + controller: controller.destinationTextEditController.value, + // backgroundColor: AppThemeData.grey50, + // borderColor: AppThemeData.grey50, + hintText: "Destination Location".tr, + enable: false, + readOnly: true, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.radio_button_checked, color: Colors.red)), + ), + ), + ], + ), + ), + Positioned( + left: 10, + top: 33, + child: DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 40), + ), + ), + ], + ), + const SizedBox(height: 14), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadiusGeometry.circular(10), + child: NetworkImageWidget(imageUrl: controller.currentOrder.value.driver?.profilePictureURL ?? '', height: 70, width: 70, borderRadius: 35), + ), + SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.currentOrder.value.driver?.fullName() ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 18), + ), + Text( + "${controller.currentOrder.value.driver?.vehicleType ?? ''} | ${controller.currentOrder.value.driver?.carMakes ?? ''}", + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 14), + ), + Text( + controller.currentOrder.value.driver?.carNumber ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 16), + ), + ], + ), + ), + RoundedButtonBorder( + title: controller.driverModel.value.averageRating.toStringAsFixed(1) ?? '', + width: 20, + height: 3.5, + radius: 10, + isRight: false, + isCenter: true, + textColor: AppThemeData.warning400, + borderColor: AppThemeData.warning400, + color: AppThemeData.warning50, + icon: SvgPicture.asset("assets/icons/ic_start.svg"), + onPress: () {}, + ), + ], + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Subtotal".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + Constant.amountShow(amount: controller.subTotal.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Discount".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(Constant.amountShow(amount: controller.discount.value.toString()), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.danger300)), + ], + ), + ), + + // Tax List + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + children: [ + Expanded( + child: Text( + '${taxModel.title} (${taxModel.tax} ${taxModel.type == "Fixed" ? Constant.currencyData!.code : "%"})'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + Constant.amountShow( + amount: Constant.calculateTax(amount: (controller.subTotal.value - controller.discount.value).toString(), taxModel: taxModel).toString(), + ).tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ); + }, + ), + const Divider(), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Order Total".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + Constant.amountShow(amount: controller.totalAmount.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(10), + child: InkWell( + onTap: () { + controller.bottomSheetType.value = 'payment'; + }, + child: Row( + children: [ + controller.selectedPaymentMethod.value == PaymentGateway.wallet.name + ? cardDecorationScreen(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png") + : controller.selectedPaymentMethod.value == PaymentGateway.cod.name + ? cardDecorationScreen(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png") + : controller.selectedPaymentMethod.value == PaymentGateway.stripe.name + ? cardDecorationScreen(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png") + : controller.selectedPaymentMethod.value == PaymentGateway.paypal.name + ? cardDecorationScreen(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payStack.name + ? cardDecorationScreen(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png") + : controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name + ? cardDecorationScreen(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png") + : controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name + ? cardDecorationScreen(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payFast.name + ? cardDecorationScreen(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png") + : controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name + ? cardDecorationScreen(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png") + : controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name + ? cardDecorationScreen(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png") + : controller.selectedPaymentMethod.value == PaymentGateway.xendit.name + ? cardDecorationScreen(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png") + : cardDecorationScreen(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + SizedBox(width: 22), + Text( + controller.selectedPaymentMethod.value.tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + Obx(() { + if (controller.currentOrder.value.status == Constant.orderInTransit) { + return Column( + children: [ + RoundedButtonFill( + title: "SOS".tr, + color: Colors.red.withOpacity(0.50), + textColor: AppThemeData.grey50, + isCenter: true, + icon: Icon(Icons.call, color: Colors.white), + onPress: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + LocationData location = await controller.currentLocation.value.getLocation(); + + await FireStoreUtils.getSOS(controller.currentOrder.value.id ?? '').then((value) async { + if (value == false) { + await FireStoreUtils.setSos(controller.currentOrder.value.id ?? '', UserLocation(latitude: location.latitude!, longitude: location.longitude!)).then((value) { + ShowToastDialog.closeLoader(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Builder( + builder: (context) { + return Text("Your SOS request has been submitted to admin".tr); + }, + ), + backgroundColor: Colors.green, + duration: const Duration(seconds: 3), + ), + ); + }); + } else { + ShowToastDialog.closeLoader(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Builder( + builder: (context) { + return Text("Your SOS request is already submitted".tr); + }, + ), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + }); + }, + ), + const SizedBox(height: 20), + ], + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ), + ), + Obx(() { + if (controller.currentOrder.value.status == Constant.orderInTransit && controller.currentOrder.value.paymentStatus == false) { + return RoundedButtonFill( + title: "Pay Now".tr, + onPress: () async { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.totalAmount.value.toString(), context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.completeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + controller.completeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.totalAmount.value.toString()), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.totalAmount.value.toString(), orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ), + ), + ); + }, + ), + ); + } + + Padding cardDecorationScreen(IntercityHomeController controller, PaymentGateway value, isDark, String image) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + width: 40, + height: 40, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: image == '' ? Container(color: isDark ? AppThemeData.grey800 : AppThemeData.grey100) : Image.asset(image)), + ), + ); + } + + Obx cardDecoration(IntercityHomeController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: controller.userModel.value.walletAmount == null ? '0.0' : controller.userModel.value.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/cab_booking_screen.dart b/lib/screen_ui/cab_service_screens/cab_booking_screen.dart new file mode 100644 index 0000000..dfb6c7c --- /dev/null +++ b/lib/screen_ui/cab_service_screens/cab_booking_screen.dart @@ -0,0 +1,1590 @@ +import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/vehicle_type.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/screen_ui/cab_service_screens/cab_coupon_code_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_border.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/utils/utils.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:latlong2/latlong.dart' as latlong; +import '../../constant/constant.dart'; +import '../../controllers/cab_booking_controller.dart'; +import '../../controllers/cab_dashboard_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/user_model.dart'; +import '../../service/fire_store_utils.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../themes/text_field_widget.dart'; +import '../../widget/osm_map/map_picker_page.dart'; +import '../../widget/place_picker/location_picker_screen.dart'; +import '../../widget/place_picker/selected_location_model.dart'; +import 'package:location/location.dart'; + +class CabBookingScreen extends StatelessWidget { + const CabBookingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CabBookingController(), + builder: (controller) { + return Scaffold( + body: + controller.isLoading.value + ? Constant.loader() + : Stack( + children: [ + Constant.selectedMapType == "osm" + ? flutterMap.FlutterMap( + mapController: controller.mapOsmController, + options: flutterMap.MapOptions( + initialCenter: + Constant.currentLocation != null + ? latlong.LatLng(Constant.currentLocation!.latitude, Constant.currentLocation!.longitude) + : controller.currentOrder.value.id != null + ? latlong.LatLng( + double.parse(controller.currentOrder.value.sourceLocation!.latitude.toString()), + double.parse(controller.currentOrder.value.sourceLocation!.longitude.toString()), + ) + : latlong.LatLng(41.4219057, -102.0840772), + initialZoom: 10, + ), + children: [ + flutterMap.TileLayer( + urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: Platform.isAndroid ? "com.emart.customer" : "com.emart.customer.ios", + ), + flutterMap.MarkerLayer(markers: controller.osmMarker), + if (controller.routePoints.isNotEmpty) flutterMap.PolylineLayer(polylines: [flutterMap.Polyline(points: controller.routePoints, strokeWidth: 5.0, color: Colors.blue)]), + ], + ) + : GoogleMap( + onMapCreated: (googleMapController) { + controller.mapController = googleMapController; + + if (Constant.currentLocation != null) { + controller.setDepartureMarker(Constant.currentLocation!.latitude, Constant.currentLocation!.longitude); + controller.searchPlaceNameGoogle(); + } + }, + initialCameraPosition: CameraPosition(target: controller.currentPosition.value, zoom: 14), + myLocationEnabled: true, + zoomControlsEnabled: true, + zoomGesturesEnabled: true, + polylines: Set.of(controller.polyLines.values), + markers: controller.markers.toSet(), // reactive marker set + ), + + Positioned( + top: 50, + left: Constant.isRtl ? null : 20, + right: Constant.isRtl ? 20 : null, + child: InkWell( + onTap: () { + if (controller.bottomSheetType.value == "vehicleSelection") { + controller.bottomSheetType.value = "location"; + } else if (controller.bottomSheetType.value == "payment") { + controller.bottomSheetType.value = "vehicleSelection"; + } else if (controller.bottomSheetType.value == "conformRide") { + controller.bottomSheetType.value = "payment"; + } else if (controller.bottomSheetType.value == "waitingDriver" || controller.bottomSheetType.value == "driverDetails") { + Get.back(result: true); + } else { + Get.back(); + } + }, + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.all(10), + child: Center(child: Icon(Icons.arrow_back_ios_new, color: isDark ? AppThemeData.grey50 : AppThemeData.greyDark50, size: 20)), + ), + ), + ), + ), + controller.bottomSheetType.value == "location" + ? searchLocationBottomSheet(context, controller, isDark) + : controller.bottomSheetType.value == "vehicleSelection" + ? vehicleSelection(context, controller, isDark) + : controller.bottomSheetType.value == "payment" + ? paymentBottomSheet(context, controller, isDark) + : controller.bottomSheetType.value == "conformRide" + ? conformBottomSheet(context, isDark) + : controller.bottomSheetType.value == "waitingForDriver" + ? waitingDialog(context, controller, isDark) + : controller.bottomSheetType.value == "driverDetails" + ? driverDialog(context, controller, isDark) + : SizedBox(), + ], + ), + ); + }, + ); + } + + Widget searchLocationBottomSheet(BuildContext context, CabBookingController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.30, + // Start height + minChildSize: 0.30, + // Minimum height + maxChildSize: 0.8, + // Maximum height + expand: false, + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(35))), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + SizedBox(height: 10), + Stack( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Pickup Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.sourceTextEditController.value.text = ''; + final firstPlace = result; + if (Constant.checkZoneCheck(firstPlace.coordinates.latitude, firstPlace.coordinates.longitude) == true) { + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.sourceTextEditController.value.text = address.toString(); + controller.setDepartureMarker(lat, lng); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + if (Constant.checkZoneCheck(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude) == true) { + controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDepartureMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + }); + } + }, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Pickup Location".tr, + enable: false, + prefix: Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Image.asset("assets/icons/pickup.png", height: 22, width: 22)), + ), + ), + const SizedBox(height: 10), + // Destination Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.destinationTextEditController.value.text = ''; + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.destinationTextEditController.value.text = address.toString(); + controller.setDestinationMarker(lat, lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.destinationTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDestinationMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } + }); + } + }, + child: TextFieldWidget( + controller: controller.destinationTextEditController.value, + // backgroundColor: AppThemeData.grey50, + // borderColor: AppThemeData.grey50, + hintText: "Destination Location".tr, + enable: false, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.radio_button_checked, color: Colors.red)), + ), + ), + ], + ), + ), + Positioned( + left: 10, + top: 33, + child: DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 40), + ), + ), + ], + ), + SizedBox(height: 15), + RoundedButtonFill( + title: "Continue".tr, + onPress: () { + if (controller.sourceTextEditController.value.text.isEmpty) { + ShowToastDialog.showToast("Please select source location".tr); + } else if (controller.destinationTextEditController.value.text.isEmpty) { + ShowToastDialog.showToast("Please select destination location".tr); + } else { + controller.bottomSheetType.value = "vehicleSelection"; + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ); + }, + ), + ); + } + + Widget vehicleSelection(BuildContext context, CabBookingController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.40, + minChildSize: 0.40, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: const BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + "Select Your Vehicle Type".tr, + style: AppThemeData.boldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + textAlign: TextAlign.start, + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: controller.vehicleTypes.length, + shrinkWrap: true, + padding: EdgeInsets.only(bottom: 20), + controller: scrollController, + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + VehicleType vehicleType = controller.vehicleTypes[index]; + return Obx( + () => InkWell( + onTap: () { + controller.selectedVehicleType.value = controller.vehicleTypes[index]; + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: + isDark + ? controller.selectedVehicleType.value.id == vehicleType.id + ? Colors.white + : AppThemeData.grey500 + : controller.selectedVehicleType.value.id == vehicleType.id + ? AppThemeData.grey300 + : Colors.transparent, + width: 1, + ), + color: + controller.selectedVehicleType.value.id == vehicleType.id + ? AppThemeData.grey50 + : isDark + ? AppThemeData.grey300 + : Colors.white, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Row( + children: [ + ClipRRect( + //borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: vehicleType.vehicleIcon.toString(), + height: 60, + width: 60, + imageBuilder: + (context, imageProvider) => + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => ClipRRect(borderRadius: BorderRadius.circular(20), child: Image.network(Constant.placeHolderImage, fit: BoxFit.cover)), + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${vehicleType.name} | ${controller.distance.toStringAsFixed(2)}${'km'.tr}", + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1), + ), + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text(controller.duration.value, style: const TextStyle(fontWeight: FontWeight.w400, letterSpacing: 1)), + ), + ], + ), + ), + ), + Text( + Constant.amountShow(amount: controller.getAmount(vehicleType).toString()), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + Obx( + () => RoundedButtonFill( + title: 'pay_amount'.trParams({ + 'amount': + controller.selectedVehicleType.value.id == null + ? Constant.amountShow(amount: "0.0") + : Constant.amountShow(amount: controller.getAmount(controller.selectedVehicleType.value).toString()), + }), + onPress: () async { + if (controller.selectedVehicleType.value.id != null) { + controller.calculateTotalAmount(); + controller.bottomSheetType.value = "payment"; + } else { + ShowToastDialog.showToast("Please select a vehicle type first.".tr); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget paymentBottomSheet(BuildContext context, CabBookingController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.70, + minChildSize: 0.30, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Select Payment Method".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Icon(Icons.close, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + controller: scrollController, + children: [ + Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + const SizedBox(height: 10), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + onPress: () async { + if (controller.selectedPaymentMethod.value.isEmpty) { + ShowToastDialog.showToast("Please select a payment method".tr); + return; + } + if (controller.selectedPaymentMethod.value == "wallet") { + num walletAmount = controller.userModel.value.walletAmount ?? 0; + if (walletAmount <= 0) { + ShowToastDialog.showToast("Insufficient wallet balance. Please select another payment method.".tr); + return; + } + } + if (controller.currentOrder.value.id != null) { + controller.bottomSheetType.value = "driverDetails"; + } else { + controller.bottomSheetType.value = "conformRide"; + } + }, + ), + ], + ), + ); + }, + ), + ); + } + + Widget conformBottomSheet(BuildContext context, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.3, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return GetX( + init: CabBookingController(), + builder: (controller) { + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + Expanded( + child: ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + const SizedBox(height: 10), + Stack( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? Colors.transparent : Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Pickup Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.sourceTextEditController.value.text = ''; + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.sourceTextEditController.value.text = address.toString(); + controller.setDepartureMarker(lat, lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDepartureMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } + }); + } + }, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Pickup Location".tr, + enable: false, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.stop_circle_outlined, color: Colors.green)), + ), + ), + const SizedBox(height: 10), + // Destination Location + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + controller.destinationTextEditController.value.text = ''; + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + controller.destinationTextEditController.value.text = address.toString(); + controller.setDestinationMarker(lat, lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.destinationTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.setDestinationMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } + }); + } + }, + child: TextFieldWidget( + controller: controller.destinationTextEditController.value, + // backgroundColor: AppThemeData.grey50, + // borderColor: AppThemeData.grey50, + hintText: "Destination Location".tr, + enable: false, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.radio_button_checked, color: Colors.red)), + ), + ), + ], + ), + ), + Positioned( + left: 10, + top: 33, + child: DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 40), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded(child: Text("Promo code".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + InkWell( + onTap: () { + Get.to(CabCouponCodeScreen())!.then((value) { + if (value != null) { + controller.couponCodeTextEditController.value.text = value.code ?? ''; + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: value); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = value; + controller.calculateTotalAmount(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } + }); + }, + child: Text( + "View All".tr, + style: AppThemeData.boldTextStyle(decoration: TextDecoration.underline, fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 5), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + width: Responsive.width(100, context), + height: Responsive.height(6, context), + color: AppThemeData.carRent50, + child: DottedBorder( + options: RectDottedBorderOptions(dashPattern: [10, 5], strokeWidth: 1, padding: EdgeInsets.all(0), color: AppThemeData.carRent400), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_coupon.svg"), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextFormField( + controller: controller.couponCodeTextEditController.value, + style: AppThemeData.semiBoldTextStyle(color: AppThemeData.parcelService500, fontSize: 16), + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'Write coupon Code'.tr, + contentPadding: EdgeInsets.only(bottom: 10), + hintStyle: AppThemeData.semiBoldTextStyle(color: AppThemeData.parcelService500, fontSize: 16), + ), + ), + ), + ), + RoundedButtonFill( + title: "Redeem now".tr, + width: 27, + borderRadius: 10, + fontSizes: 14, + onPress: () async { + if (controller.couponCodeTextEditController.value.text.trim().isEmpty) { + ShowToastDialog.showToast("Please enter a coupon code".tr); + return; + } + + List matchedCoupons = + controller.cabCouponList + .where((element) => element.code!.toLowerCase().trim() == controller.couponCodeTextEditController.value.text.toLowerCase().trim()) + .toList(); + + if (matchedCoupons.isNotEmpty) { + CouponModel couponModel = matchedCoupons.first; + + if (couponModel.expiresAt != null && couponModel.expiresAt!.toDate().isAfter(DateTime.now())) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: couponModel); + + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = couponModel; + controller.discount.value = couponAmount; + controller.calculateTotalAmount(); + ShowToastDialog.showToast("Coupon applied successfully".tr); + controller.update(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } else { + ShowToastDialog.showToast("This coupon code has been expired".tr); + } + } else { + ShowToastDialog.showToast("Invalid coupon code".tr); + } + }, + color: AppThemeData.parcelService300, + textColor: AppThemeData.grey50, + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 10), + + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Subtotal".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text( + Constant.amountShow(amount: controller.subTotal.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text("Discount".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + SizedBox(width: 5), + Text( + controller.selectedCouponModel.value.id == null ? "" : "(${controller.selectedCouponModel.value.code})", + style: AppThemeData.mediumTextStyle(fontSize: 16, color: AppThemeData.primary300), + ), + ], + ), + Text(Constant.amountShow(amount: controller.discount.value.toString()), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.danger300)), + ], + ), + ), + + // Tax List + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + children: [ + Expanded( + child: Text( + '${taxModel.title} (${taxModel.tax} ${taxModel.type == "Fixed" ? Constant.currencyData!.code : "%"})'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + ), + ), + Text( + Constant.amountShow( + amount: Constant.calculateTax(amount: (controller.subTotal.value - controller.discount.value).toString(), taxModel: taxModel).toString(), + ).tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ); + }, + ), + const Divider(), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Order Total".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + Constant.amountShow(amount: controller.totalAmount.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(10), + child: Row( + children: [ + controller.selectedPaymentMethod.value == '' + ? cardDecorationScreen(controller, PaymentGateway.wallet, isDark, "") + : controller.selectedPaymentMethod.value == PaymentGateway.wallet.name + ? cardDecorationScreen(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png") + : controller.selectedPaymentMethod.value == PaymentGateway.cod.name + ? cardDecorationScreen(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png") + : controller.selectedPaymentMethod.value == PaymentGateway.stripe.name + ? cardDecorationScreen(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png") + : controller.selectedPaymentMethod.value == PaymentGateway.paypal.name + ? cardDecorationScreen(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payStack.name + ? cardDecorationScreen(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png") + : controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name + ? cardDecorationScreen(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png") + : controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name + ? cardDecorationScreen(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payFast.name + ? cardDecorationScreen(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png") + : controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name + ? cardDecorationScreen(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png") + : controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name + ? cardDecorationScreen(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png") + : controller.selectedPaymentMethod.value == PaymentGateway.xendit.name + ? cardDecorationScreen(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png") + : cardDecorationScreen(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + SizedBox(width: 22), + Text( + controller.selectedPaymentMethod.value.tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + RoundedButtonFill( + title: "Confirm Booking".tr, + onPress: () async { + controller.placeOrder(); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } + + Widget waitingDialog(BuildContext context, CabBookingController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.4, + minChildSize: 0.4, + maxChildSize: 0.4, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + SizedBox(height: 30), + Text("Waiting for driver....".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: AppThemeData.grey900)), + Image.asset('assets/loader.gif', width: 250), + RoundedButtonFill( + title: "Cancel Ride".tr, + color: AppThemeData.danger300, + textColor: AppThemeData.surface, + onPress: () async { + try { + controller.currentOrder.update((order) { + if (order != null) { + order.status = Constant.orderRejected; + } + }); + + if (controller.currentOrder.value.id != null) { + await FireStoreUtils.updateCabOrder(controller.currentOrder.value); + } + + controller.bottomSheetType.value = ""; + controller.polyLines.clear(); + controller.markers.clear(); + controller.osmMarker.clear(); + controller.routePoints.clear(); + controller.sourceTextEditController.value.clear(); + controller.destinationTextEditController.value.clear(); + controller.departureLatLong.value = const LatLng(0.0, 0.0); + controller.destinationLatLong.value = const LatLng(0.0, 0.0); + controller.departureLatLongOsm.value = latlong.LatLng(0.0, 0.0); + controller.destinationLatLongOsm.value = latlong.LatLng(0.0, 0.0); + + // 4. Reset user’s in-progress order + if (Constant.userModel != null) { + Constant.userModel!.inProgressOrderID = null; + await FireStoreUtils.updateUser(Constant.userModel!); + } + ShowToastDialog.showToast("Ride cancelled successfully".tr); + // Get.offAll(const CabDashboardScreen()); + Get.back(); + CabDashboardController cabDashboardController = Get.put(CabDashboardController()); + cabDashboardController.selectedIndex.value = 0; + } catch (e) { + ShowToastDialog.showToast("Failed to cancel ride".tr); + } + }, + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget driverDialog(BuildContext context, CabBookingController controller, bool isDark) { + return Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.3, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.grey400), height: 4, width: 33), + Expanded( + child: ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + const SizedBox(height: 10), + Stack( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? Colors.transparent : Colors.white, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () async { + // if (Constant.selectedMapType == 'osm') { + // final result = await Get.to(() => MapPickerPage()); + // if (result != null) { + // controller.sourceTextEditController.value.text = ''; + // final firstPlace = result; + // final lat = firstPlace.coordinates.latitude; + // final lng = firstPlace.coordinates.longitude; + // final address = firstPlace.address; + // controller.sourceTextEditController.value.text = address.toString(); + // controller.setDepartureMarker(lat, lng); + // } + // } else { + // Get.to(LocationPickerScreen())!.then((value) async { + // if (value != null) { + // SelectedLocationModel selectedLocationModel = value; + // + // controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + // controller.setDepartureMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + // } + // }); + // } + }, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Pickup Location".tr, + enable: false, + readOnly: true, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.stop_circle_outlined, color: Colors.green)), + ), + ), + const SizedBox(height: 10), + InkWell( + onTap: () async { + // if (Constant.selectedMapType == 'osm') { + // final result = await Get.to(() => MapPickerPage()); + // if (result != null) { + // controller.destinationTextEditController.value.text = ''; + // final firstPlace = result; + // final lat = firstPlace.coordinates.latitude; + // final lng = firstPlace.coordinates.longitude; + // final address = firstPlace.address; + // controller.destinationTextEditController.value.text = address.toString(); + // controller.setDestinationMarker(lat, lng); + // } + // } else { + // Get.to(LocationPickerScreen())!.then((value) async { + // if (value != null) { + // SelectedLocationModel selectedLocationModel = value; + // + // controller.destinationTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + // controller.setDestinationMarker(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + // } + // }); + // } + }, + child: TextFieldWidget( + controller: controller.destinationTextEditController.value, + // backgroundColor: AppThemeData.grey50, + // borderColor: AppThemeData.grey50, + hintText: "Destination Location".tr, + enable: false, + readOnly: true, + prefix: const Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.radio_button_checked, color: Colors.red)), + ), + ), + ], + ), + ), + Positioned( + left: 10, + top: 33, + child: DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 40), + ), + ), + ], + ), + const SizedBox(height: 14), + + Constant.isEnableOTPTripStart == true + ? Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Otp :".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text( + controller.currentOrder.value.otpCode ?? '', + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ) + : SizedBox.shrink(), + + if (Constant.isEnableOTPTripStart == true) SizedBox(height: 14), + + controller.currentOrder.value.driver != null + ? Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadiusGeometry.circular(10), + child: NetworkImageWidget(imageUrl: controller.currentOrder.value.driver?.profilePictureURL ?? '', height: 70, width: 70, borderRadius: 35), + ), + SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.currentOrder.value.driver?.fullName() ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 18), + ), + Text( + "${controller.currentOrder.value.driver?.vehicleType ?? ''} | ${controller.currentOrder.value.driver?.carMakes.toString()}", + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 14), + ), + Text( + controller.currentOrder.value.driver?.carNumber ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 16), + ), + ], + ), + ), + Column( + children: [ + RoundedButtonBorder( + title: controller.driverModel.value.averageRating.toStringAsFixed(1) ?? '', + width: 20, + height: 3.5, + radius: 10, + isRight: false, + isCenter: true, + textColor: AppThemeData.warning400, + borderColor: AppThemeData.warning400, + color: AppThemeData.warning50, + icon: SvgPicture.asset("assets/icons/ic_start.svg"), + onPress: () {}, + ), + SizedBox(height: 10), + Row( + children: [ + InkWell( + onTap: () { + Constant.makePhoneCall(controller.currentOrder.value.driver!.phoneNumber.toString()); + }, + child: Container( + width: 38, + height: 38, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey200 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + SizedBox(width: 10), + InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.currentOrder.value.authorID ?? ''); + UserModel? driverUser = await FireStoreUtils.getUserProfile(controller.currentOrder.value.driverId ?? ''); + + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer?.fullName(), + "restaurantName": driverUser?.fullName(), + "orderId": controller.currentOrder.value.id, + "restaurantId": driverUser?.id, + "customerId": customer?.id, + "customerProfileImage": customer?.profilePictureURL, + "restaurantProfileImage": driverUser?.profilePictureURL, + "token": driverUser?.fcmToken, + "chatType": "Driver", + }, + ); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey200 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ), + ], + ), + ], + ) + : SizedBox(), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(10), + child: InkWell( + onTap: () { + controller.bottomSheetType.value = 'payment'; + }, + child: Row( + children: [ + controller.selectedPaymentMethod.value == PaymentGateway.wallet.name + ? cardDecorationScreen(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png") + : controller.selectedPaymentMethod.value == PaymentGateway.cod.name + ? cardDecorationScreen(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png") + : controller.selectedPaymentMethod.value == PaymentGateway.stripe.name + ? cardDecorationScreen(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png") + : controller.selectedPaymentMethod.value == PaymentGateway.paypal.name + ? cardDecorationScreen(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payStack.name + ? cardDecorationScreen(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png") + : controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name + ? cardDecorationScreen(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png") + : controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name + ? cardDecorationScreen(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payFast.name + ? cardDecorationScreen(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png") + : controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name + ? cardDecorationScreen(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png") + : controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name + ? cardDecorationScreen(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png") + : controller.selectedPaymentMethod.value == PaymentGateway.xendit.name + ? cardDecorationScreen(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png") + : cardDecorationScreen(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + SizedBox(width: 22), + Expanded( + child: Text( + controller.selectedPaymentMethod.value.tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text("Change".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300)), + ], + ), + ), + ), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Subtotal".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text( + Constant.amountShow(amount: controller.subTotal.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Discount".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(Constant.amountShow(amount: controller.discount.value.toString()), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.danger300)), + ], + ), + ), + + // Tax List + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + children: [ + Expanded( + child: Text( + '${taxModel.title} (${taxModel.tax} ${taxModel.type == "Fixed" ? Constant.currencyData!.code : "%"})'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + ), + ), + Text( + Constant.amountShow( + amount: Constant.calculateTax(amount: (controller.subTotal.value - controller.discount.value).toString(), taxModel: taxModel).toString(), + ).tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ); + }, + ), + const Divider(), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Order Total".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + Constant.amountShow(amount: controller.totalAmount.value.toString()), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + + Obx(() { + if (controller.currentOrder.value.status == Constant.orderInTransit) { + return Column( + children: [ + RoundedButtonFill( + title: "SOS".tr, + color: Colors.red.withOpacity(0.50), + textColor: AppThemeData.grey50, + isCenter: true, + icon: const Icon(Icons.call, color: Colors.white), + onPress: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + LocationData location = await controller.currentLocation.value.getLocation(); + + await FireStoreUtils.getSOS(controller.currentOrder.value.id ?? '').then((value) async { + if (value == false) { + await FireStoreUtils.setSos(controller.currentOrder.value.id ?? '', UserLocation(latitude: location.latitude!, longitude: location.longitude!)).then((_) { + ShowToastDialog.closeLoader(); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Your SOS request has been submitted to admin".tr), backgroundColor: Colors.green, duration: Duration(seconds: 3))); + }); + } else { + ShowToastDialog.closeLoader(); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Your SOS request is already submitted".tr), backgroundColor: Colors.red, duration: Duration(seconds: 3))); + } + }); + }, + ), + const SizedBox(height: 10), + ], + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.currentOrder.value.status == Constant.orderInTransit && controller.currentOrder.value.paymentStatus == false) { + return RoundedButtonFill( + title: "Pay Now".tr, + onPress: () async { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.totalAmount.value.toString(), context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.completeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + if (Constant.userModel!.walletAmount == null || Constant.userModel!.walletAmount! < controller.totalAmount.value) { + ShowToastDialog.showToast("You do not have sufficient wallet balance".tr); + } else { + controller.completeOrder(); + } + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.totalAmount.value.toString()), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.totalAmount.value.toString(), orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ), + ), + ); + }, + ), + ); + } + + Padding cardDecorationScreen(CabBookingController controller, PaymentGateway value, isDark, String image) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + width: 40, + height: 40, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: image == '' ? Container(color: isDark ? AppThemeData.grey800 : AppThemeData.grey100) : Image.asset(image)), + ), + ); + } + + Obx cardDecoration(CabBookingController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: controller.userModel.value.walletAmount == null ? '0.0' : controller.userModel.value.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/cab_coupon_code_screen.dart b/lib/screen_ui/cab_service_screens/cab_coupon_code_screen.dart new file mode 100644 index 0000000..e69a795 --- /dev/null +++ b/lib/screen_ui/cab_service_screens/cab_coupon_code_screen.dart @@ -0,0 +1,138 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cab_coupon_code_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/widget/my_separator.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class CabCouponCodeScreen extends StatelessWidget { + const CabCouponCodeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CabCouponCodeController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Coupon".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : controller.cabCouponList.isEmpty + ? Constant.showEmptyView(message: "Coupon not found".tr) + : ListView.builder( + shrinkWrap: true, + itemCount: controller.cabCouponList.length, + itemBuilder: (context, index) { + CouponModel couponModel = controller.cabCouponList[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Container( + height: Responsive.height(16, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + child: Stack( + children: [ + Image.asset("assets/images/ic_coupon_image.png", height: Responsive.height(16, context), fit: BoxFit.fill), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Align( + alignment: Alignment.center, + child: RotatedBox( + quarterTurns: -1, + child: Text( + "${couponModel.discountType == "Fix Price" ? Constant.amountShow(amount: couponModel.discount) : "${couponModel.discount}%"} ${'Off'.tr}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(6), color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "${couponModel.code}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + const Expanded(child: SizedBox(height: 10)), + InkWell( + onTap: () { + Get.back(result: couponModel); + }, + child: Text( + "Tap To Apply".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 20), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 20), + Text( + "${couponModel.description}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/cab_dashboard_screen.dart b/lib/screen_ui/cab_service_screens/cab_dashboard_screen.dart new file mode 100644 index 0000000..d87addb --- /dev/null +++ b/lib/screen_ui/cab_service_screens/cab_dashboard_screen.dart @@ -0,0 +1,80 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cab_dashboard_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +class CabDashboardScreen extends StatelessWidget { + const CabDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: CabDashboardController(), + builder: (controller) { + return Scaffold( + body: controller.pageList[controller.selectedIndex.value], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + showUnselectedLabels: true, + showSelectedLabels: true, + selectedFontSize: 12, + selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + currentIndex: controller.selectedIndex.value, + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + selectedItemColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + unselectedItemColor: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + onTap: (int index) { + if (index == 0) { + Get.put(CabDashboardController()); + } + controller.selectedIndex.value = index; + }, + items: + Constant.walletSetting == false + ? [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ] + : [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_wallet_cab.svg", label: 'Wallet'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ], + ), + ); + }, + ); + }); + } + + BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required CabDashboardController controller}) { + return BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: SvgPicture.asset( + assetIcon, + height: 22, + width: 22, + color: + controller.selectedIndex.value == index + ? isDark + ? AppThemeData.primary300 + : AppThemeData.primary300 + : isDark + ? AppThemeData.grey300 + : AppThemeData.grey600, + ), + ), + label: label, + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/cab_home_screen.dart b/lib/screen_ui/cab_service_screens/cab_home_screen.dart new file mode 100644 index 0000000..fb21871 --- /dev/null +++ b/lib/screen_ui/cab_service_screens/cab_home_screen.dart @@ -0,0 +1,285 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cab_home_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/banner_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import 'Intercity_home_screen.dart'; +import 'cab_booking_screen.dart'; + +class CabHomeScreen extends StatelessWidget { + const CabHomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CabHomeController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell( + onTap: () { + Get.offAll(const LoginScreen()); + }, + child: Text( + "Login".tr, + textAlign: TextAlign.center, + style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 12), + ), + ) + : Text( + Constant.userModel!.fullName(), + textAlign: TextAlign.center, + style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 12), + ), + Text( + Constant.selectedLocation.getFullAddress(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BannerView(bannerList: controller.bannerTopHome), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 20), + Text( + "Where are you going for?".tr, + style: AppThemeData.mediumTextStyle( + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + fontSize: 18, + ), + ), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.sectionConstantModel!.rideType == "both" || Constant.sectionConstantModel!.rideType == "ride" + ? GestureDetector( + onTap: () { + Get.to(() => CabBookingScreen()); + }, + child: Container( + width: Responsive.width(40, context), + decoration: BoxDecoration( + color: AppThemeData.warning50, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppThemeData.warning200), + ), + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_ride.svg", height: 38, width: 38), + SizedBox(height: 20), + Text( + "Ride".tr, + style: AppThemeData.semiBoldTextStyle(color: AppThemeData.taxiBooking500, fontSize: 16), + ), + Text( + "City rides, 24x7 availability".tr, + style: AppThemeData.mediumTextStyle(color: AppThemeData.taxiBooking600, fontSize: 14), + ), + ], + ), + ), + ) + : SizedBox(), + SizedBox(width: 20), + Constant.sectionConstantModel!.rideType == "both" || Constant.sectionConstantModel!.rideType == "intercity" + ? GestureDetector( + onTap: () { + Get.to(() => IntercityHomeScreen()); + }, + child: Container( + width: Responsive.width(44, context), + decoration: BoxDecoration( + color: AppThemeData.carRent50, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppThemeData.carRent200), + ), + padding: EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_intercity.svg", height: 38, width: 38), + SizedBox(height: 20), + Text( + "Intercity/Outstation".tr, + style: AppThemeData.semiBoldTextStyle(color: AppThemeData.carRent500, fontSize: 16), + ), + Text( + "Long trips, prepaid options".tr, + style: AppThemeData.mediumTextStyle(color: AppThemeData.parcelService600, fontSize: 14), + ), + ], + ), + ), + ) + : SizedBox(), + ], + ), + SizedBox(height: 30), + Row( + children: [ + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Every Ride. Every Driver. Verified.".tr, + style: AppThemeData.boldTextStyle( + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + fontSize: 22, + ), + ), + Text( + "All drivers go through ID checks and background verification for your safety.".tr, + style: AppThemeData.mediumTextStyle( + color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, + fontSize: 14, + ), + ), + ], + ), + ), + Expanded(child: Image.asset("assets/images/img_ride_driver.png", height: 118, width: 68)), + ], + ), + ], + ), + ], + ), + ), + ); + }, + ); + } +} + +class BannerView extends StatelessWidget { + final List bannerList; + final RxInt currentPage = 0.obs; + final ScrollController scrollController = ScrollController(); + + BannerView({super.key, required this.bannerList}); + + /// Computes the visible item index from scroll offset + void onScroll(BuildContext context) { + if (scrollController.hasClients && bannerList.isNotEmpty) { + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = screenWidth * 0.8 + 10; // banner width + spacing + final offset = scrollController.offset; + final index = (offset / itemWidth).round(); + + if (index != currentPage.value && index < bannerList.length) { + currentPage.value = index; + } + } + } + + @override + Widget build(BuildContext context) { + scrollController.addListener(() { + onScroll(context); + }); + + return bannerList.isEmpty + ? SizedBox() + : Column( + children: [ + SizedBox(height: 20), + SizedBox( + height: 150, + child: ListView.separated( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: bannerList.length, + separatorBuilder: (context, index) => const SizedBox(width: 15), + itemBuilder: (context, index) { + final banner = bannerList[index]; + return ClipRRect( + borderRadius: BorderRadius.circular(15), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: NetworkImageWidget(imageUrl: banner.photo ?? '', fit: BoxFit.cover), + ), + ); + }, + ), + ), + const SizedBox(height: 8), + Obx(() { + return Row( + children: List.generate(bannerList.length, (index) { + bool isSelected = currentPage.value == index; + return Expanded( + child: Container( + height: 4, + decoration: BoxDecoration( + color: isSelected ? AppThemeData.grey300 : AppThemeData.grey100, + borderRadius: BorderRadius.circular(5), + ), + ), + ); + }), + ); + }), + ], + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/cab_order_details.dart b/lib/screen_ui/cab_service_screens/cab_order_details.dart new file mode 100644 index 0000000..40bbfbb --- /dev/null +++ b/lib/screen_ui/cab_service_screens/cab_order_details.dart @@ -0,0 +1,497 @@ +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/cab_order_details_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/user_model.dart'; +import '../../service/fire_store_utils.dart'; +import '../../themes/app_them_data.dart'; +import 'package:flutter_map/flutter_map.dart' as fm; +import 'package:google_maps_flutter/google_maps_flutter.dart' as gmap; +import 'package:latlong2/latlong.dart' as osm; + +import '../../themes/round_button_border.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../utils/network_image_widget.dart'; +import '../multi_vendor_service/chat_screens/chat_screen.dart'; +import 'cab_review_screen.dart'; +import 'complain_screen.dart'; + +class CabOrderDetails extends StatelessWidget { + const CabOrderDetails({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CabOrderDetailsController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Ride Details".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + width: double.infinity, + padding: const EdgeInsets.all(16), + child: Text( + "${'Order Id:'.tr} ${Constant.orderId(orderId: controller.cabOrder.value.id.toString())}".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const SizedBox(height: 16), + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${'Booking Date:'.tr} ${controller.formatDate(controller.cabOrder.value.scheduleDateTime!)}".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Icon(Icons.stop_circle_outlined, color: Colors.green), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 55), + ), + Icon(Icons.radio_button_checked, color: Colors.red), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Source Location Name + Expanded( + child: Text( + controller.cabOrder.value.sourceLocationName.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppThemeData.warning300, width: 1), + color: AppThemeData.warning50, + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: Text( + controller.cabOrder.value.status.toString(), + style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.warning500), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 15), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(0, size.height / 2) // start from left center + ..lineTo(size.width, size.height / 2), // draw to right center + ), + child: const SizedBox(width: 295, height: 3), + ), + SizedBox(height: 15), + Text( + controller.cabOrder.value.destinationLocationName.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ], + ), + ), + // map view show + Container( + height: 180, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: + Constant.selectedMapType == "osm" + ? fm.FlutterMap( + options: fm.MapOptions( + initialCenter: osm.LatLng(controller.cabOrder.value.sourceLocation!.latitude!, controller.cabOrder.value.sourceLocation!.longitude!), + initialZoom: 13, + ), + children: [ + fm.TileLayer(urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png"), + + // Only show polyline if points exist + if (controller.osmPolyline.isNotEmpty) fm.PolylineLayer(polylines: [fm.Polyline(points: controller.osmPolyline.toList(), color: Colors.blue, strokeWidth: 4)]), + + fm.MarkerLayer( + markers: [ + fm.Marker( + point: osm.LatLng(controller.cabOrder.value.sourceLocation!.latitude!, controller.cabOrder.value.sourceLocation!.longitude!), + width: 20, + height: 20, + child: Image.asset('assets/icons/ic_cab_pickup.png', width: 10, height: 10), + ), + fm.Marker( + point: osm.LatLng(controller.cabOrder.value.destinationLocation!.latitude!, controller.cabOrder.value.destinationLocation!.longitude!), + width: 20, + height: 20, + child: Image.asset('assets/icons/ic_cab_destination.png', width: 10, height: 10), + ), + ], + ), + ], + ) + : gmap.GoogleMap( + initialCameraPosition: gmap.CameraPosition( + target: gmap.LatLng(controller.cabOrder.value.sourceLocation!.latitude!, controller.cabOrder.value.sourceLocation!.longitude!), + zoom: 13, + ), + polylines: controller.googlePolylines.toSet(), + markers: controller.googleMarkers.toSet(), + ), + ), + ), + controller.cabOrder.value.driver != null + ? Column( + children: [ + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Ride & Fare Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: 52, + height: 52, + child: ClipRRect( + borderRadius: BorderRadiusGeometry.circular(10), + child: NetworkImageWidget(imageUrl: controller.cabOrder.value.driver?.profilePictureURL ?? '', height: 70, width: 70, borderRadius: 35), + ), + ), + SizedBox(width: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.cabOrder.value.driver?.fullName() ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 18), + ), + Text( + "${controller.cabOrder.value.driver?.vehicleType ?? ''} | ${controller.cabOrder.value.driver?.carMakes.toString()}", + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 14), + ), + Text( + controller.cabOrder.value.driver?.carNumber ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 16), + ), + ], + ), + ], + ), + RoundedButtonBorder( + title: controller.driverUser.value.averageRating.toStringAsFixed(1) ?? '', + width: 20, + height: 3.5, + radius: 10, + isRight: false, + isCenter: true, + textColor: AppThemeData.warning400, + borderColor: AppThemeData.warning400, + color: AppThemeData.warning50, + icon: SvgPicture.asset("assets/icons/ic_start.svg"), + onPress: () {}, + ), + ], + ), + Row( + children: [ + Expanded( + child: Visibility( + visible: controller.cabOrder.value.status == Constant.orderCompleted ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: controller.ratingModel.value.id != null && controller.ratingModel.value.id!.isNotEmpty ? 'Update Review'.tr : 'Add Review'.tr, + onPress: () async { + final result = await Get.to(() => CabReviewScreen(), arguments: {'order': controller.cabOrder.value}); + + // If review was submitted successfully + if (result == true) { + await controller.fetchDriverDetails(); + } + }, + height: 5, + borderRadius: 15, + color: Colors.orange, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + ), + ), + ), + SizedBox(width: 5), + Expanded( + child: Visibility( + visible: controller.cabOrder.value.status == Constant.orderCompleted ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: 'Complain'.tr, + onPress: () async { + Get.to(() => ComplainScreen(), arguments: {'order': controller.cabOrder.value}); + }, + height: 5, + borderRadius: 15, + color: Colors.orange, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + ), + ), + ), + ], + ), + if (controller.cabOrder.value.status != Constant.orderCompleted) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Constant.makePhoneCall(controller.cabOrder.value.driver!.phoneNumber.toString()); + }, + child: Container( + width: 150, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.cabOrder.value.authorID ?? ''); + UserModel? driverUser = await FireStoreUtils.getUserProfile(controller.cabOrder.value.driverId ?? ''); + + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer?.fullName(), + "restaurantName": driverUser?.fullName(), + "orderId": controller.cabOrder.value.id, + "restaurantId": driverUser?.id, + "customerId": customer?.id, + "customerProfileImage": customer?.profilePictureURL, + "restaurantProfileImage": driverUser?.profilePictureURL, + "token": driverUser?.fcmToken, + "chatType": "Driver", + }, + ); + }, + child: Container( + width: 150, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ), + ], + ), + ), + ], + ) + : SizedBox(), + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _iconTile("${double.parse(controller.cabOrder.value.distance.toString()).toStringAsFixed(2)} ${'KM'.tr}", "Distance".tr, "assets/icons/ic_distance_parcel.svg", isDark), + _iconTile(controller.cabOrder.value.duration ?? '--', "Duration".tr, "assets/icons/ic_duration.svg", isDark), + _iconTile(Constant.amountShow(amount: controller.cabOrder.value.subTotal), "${controller.cabOrder.value.paymentMethod}".tr, "assets/icons/ic_rate_parcel.svg", isDark), + ], + ), + ), + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey500)), + const SizedBox(height: 8), + + // Subtotal + _summaryTile("Subtotal", Constant.amountShow(amount: controller.subTotal.value.toString()), isDark), + + // Discount + _summaryTile("Discount", Constant.amountShow(amount: controller.discount.value.toString()), isDark), + + // Tax List + ...List.generate(controller.cabOrder.value.taxSetting!.length, (index) { + return _summaryTile( + "${controller.cabOrder.value.taxSetting![index].title} ${controller.cabOrder.value.taxSetting![index].type == 'fix' ? '' : '(${controller.cabOrder.value.taxSetting![index].tax}%)'}", + Constant.amountShow( + amount: + Constant.getTaxValue( + amount: + ((double.tryParse(controller.cabOrder.value.subTotal.toString()) ?? 0.0) - (double.tryParse(controller.cabOrder.value.discount.toString()) ?? 0.0)) + .toString(), + taxModel: controller.cabOrder.value.taxSetting![index], + ).toString(), + ), + isDark, + ); + }), + + const Divider(), + + // Total + _summaryTile("Order Total", Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark), + ], + ), + ), + const SizedBox(height: 24), + ], + ), + ), + ); + }, + ); + } + + Widget _iconTile(String value, title, icon, bool isDark) { + return Column( + children: [ + // Icon(icon, color: AppThemeData.primary300), + SvgPicture.asset(icon, height: 28, width: 28, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + const SizedBox(height: 6), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + const SizedBox(height: 6), + Text(title, style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ); + } + + Widget _summaryTile(String title, String value, bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title.tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: title == "Order Total" ? 18 : 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/cab_review_screen.dart b/lib/screen_ui/cab_service_screens/cab_review_screen.dart new file mode 100644 index 0000000..7cdece4 --- /dev/null +++ b/lib/screen_ui/cab_service_screens/cab_review_screen.dart @@ -0,0 +1,157 @@ +import 'package:customer/controllers/cab_review_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import '../../utils/network_image_widget.dart'; + +class CabReviewScreen extends StatelessWidget { + const CabReviewScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: CabReviewController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + elevation: 0, + backgroundColor: AppThemeData.primary300, + leading: GestureDetector(onTap: () => Get.back(), child: Icon(Icons.arrow_back_ios, color: isDark ? Colors.white : Colors.black)), + title: Text(controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 16)), + ), + body: Obx( + () => + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.only(top: 20), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 50, bottom: 20), + child: Card( + elevation: 2, + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 65), + child: Column( + children: [ + // Driver Name + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + controller.order.value!.driver?.fullName() ?? "", + style: TextStyle(color: isDark ? Colors.white : Colors.black87, fontFamily: AppThemeData.medium, fontSize: 18), + ), + ), + // Car info + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + controller.driverUser.value?.carNumber?.toUpperCase() ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black87, fontFamily: AppThemeData.medium), + ), + const SizedBox(width: 8), + Text( + "${controller.driverUser.value?.carName} ${controller.driverUser.value?.carMakes}", + style: TextStyle(color: isDark ? Colors.white : Colors.black38, fontFamily: AppThemeData.medium), + ), + ], + ), + + const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: Divider(color: Colors.grey)), + + // Title + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text('How is your trip?'.tr, style: TextStyle(fontSize: 18, color: isDark ? Colors.white : Colors.black, fontWeight: FontWeight.bold, letterSpacing: 2)), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'Your feedback will help us improve \n driving experience better'.tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? Colors.white : Colors.black.withOpacity(0.60), letterSpacing: 0.8), + ), + ), + + // Rating + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Rate for'.tr, style: TextStyle(fontSize: 16, color: isDark ? Colors.white : Colors.black.withOpacity(0.60), letterSpacing: 0.8)), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + controller.order.value!.driver?.fullName() ?? "", + style: TextStyle(fontSize: 18, color: isDark ? Colors.white : Colors.black, fontWeight: FontWeight.bold, letterSpacing: 2), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: RatingBar.builder( + initialRating: controller.ratings.value, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemBuilder: (context, _) => const Icon(Icons.star, color: Colors.amber), + unratedColor: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400, + onRatingUpdate: (rating) => controller.ratings.value = rating, + ), + ), + + // Comment + Padding(padding: const EdgeInsets.all(20.0), child: TextFieldWidget(hintText: "Type comment....".tr, controller: controller.comment.value, maxLine: 5)), + + // Submit Button + Padding( + padding: const EdgeInsets.all(20.0), + child: RoundedButtonFill( + title: controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, + color: AppThemeData.primary300, + textColor: isDark ? Colors.white : Colors.black, + onPress: controller.submitReview, + ), + ), + ], + ), + ), + ), + ), + ), + Align( + alignment: Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.15), blurRadius: 8, spreadRadius: 6)], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(60), + child: NetworkImageWidget(imageUrl: controller.order.value?.driver?.profilePictureURL ?? '', fit: BoxFit.cover, height: 110, width: 110), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/complain_screen.dart b/lib/screen_ui/cab_service_screens/complain_screen.dart new file mode 100644 index 0000000..5d12323 --- /dev/null +++ b/lib/screen_ui/cab_service_screens/complain_screen.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/complain_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../constant/constant.dart'; +import '../../themes/text_field_widget.dart'; + +class ComplainScreen extends StatelessWidget { + const ComplainScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetBuilder( + init: ComplainController(), + builder: (controller) { + return Obx( + () => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.taxiBooking300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: const Center(child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20)), + ), + ), + const SizedBox(width: 10), + Text("Complain".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Obx(() => TextFieldWidget(title: "Title".tr, hintText: "Title".tr, controller: controller.title.value)), + const SizedBox(height: 10), + Obx(() => TextFieldWidget(title: "Complain".tr, hintText: 'Type Description....'.tr, controller: controller.comment.value, maxLine: 8)), + const SizedBox(height: 20), + RoundedButtonFill(title: "Save".tr, color: AppThemeData.primary300, textColor: AppThemeData.grey50, onPress: () => controller.submitComplain()), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/cab_service_screens/my_cab_booking_screen.dart b/lib/screen_ui/cab_service_screens/my_cab_booking_screen.dart new file mode 100644 index 0000000..2df32bc --- /dev/null +++ b/lib/screen_ui/cab_service_screens/my_cab_booking_screen.dart @@ -0,0 +1,481 @@ +import 'package:customer/models/cab_order_model.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/my_cab_booking_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import 'package:dotted_border/dotted_border.dart'; + +import 'cab_order_details.dart'; + +class MyCabBookingScreen extends StatelessWidget { + const MyCabBookingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: MyCabBookingController(), + builder: (controller) { + return DefaultTabController( + // length: controller.tabTitles.length, + // initialIndex: controller.tabTitles.indexOf(controller.selectedTab.value), + length: controller.tabKeys.length, + initialIndex: controller.tabKeys.indexOf(controller.selectedTab.value), + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row(children: [const SizedBox(width: 10), Text("Ride History".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900))]), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(48), + child: TabBar( + isScrollable: false, + onTap: (index) { + controller.selectTab(controller.tabKeys[index]); + }, + indicatorColor: AppThemeData.taxiBooking500, + labelColor: AppThemeData.taxiBooking500, + unselectedLabelColor: AppThemeData.taxiBooking500, + labelStyle: AppThemeData.boldTextStyle(fontSize: 14), + unselectedLabelStyle: AppThemeData.mediumTextStyle(fontSize: 14), + tabs: + controller.tabKeys + .map( + (key) => Tab( + child: SizedBox.expand( + child: Center( + child: Text( + controller.getLocalizedTabTitle(key), + textAlign: TextAlign.center, + overflow: TextOverflow.visible, // 👈 show full text + ), + ), + ), + ), + ) + .toList(), + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : TabBarView( + children: + controller.tabKeys.map((title) { + final orders = controller.getOrdersForTab(title); + + if (orders.isEmpty) { + return Center(child: Text("No order found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: orders.length, + itemBuilder: (context, index) { + CabOrderModel order = orders[index]; + return GestureDetector( + onTap: () { + Get.to(() => CabOrderDetails(), arguments: {"cabOrderModel": order}); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${'Booking Date:'.tr} ${controller.formatDate(order.scheduleDateTime!)}".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Icon(Icons.stop_circle_outlined, color: Colors.green), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 55), + ), + Icon(Icons.radio_button_checked, color: Colors.red), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Source Location Name + Expanded( + child: Text( + order.sourceLocationName.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppThemeData.warning300, width: 1), + color: AppThemeData.warning50, + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: Text( + order.status.toString(), + style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.warning500), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 15), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(0, size.height / 2) // start from left center + ..lineTo(size.width, size.height / 2), // draw to right center + ), + child: const SizedBox(width: 295, height: 3), + ), + SizedBox(height: 15), + Text( + order.destinationLocationName.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + if (Constant.isEnableOTPTripStart == true) + Row( + //mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Otp :".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + SizedBox(width: 5), + Text(order.otpCode ?? '', style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + if (order.status == Constant.orderInTransit && order.paymentStatus == false) SizedBox(height: 14), + order.status == Constant.orderInTransit && order.paymentStatus == false + ? RoundedButtonFill( + title: "Pay Now".tr, + onPress: () async { + controller.selectedPaymentMethod.value = order.paymentMethod.toString(); + controller.calculateTotalAmount(order); + Get.bottomSheet(paymentBottomSheet(context, controller, isDark), isScrollControlled: true, backgroundColor: Colors.transparent); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ) + : SizedBox(), + ], + ), + ), + ); + }, + ); + }).toList(), + ), + ), + ); + }, + ); + } + + Widget paymentBottomSheet(BuildContext context, MyCabBookingController controller, bool isDark) { + return DraggableScrollableSheet( + initialChildSize: 0.70, + // Start height + minChildSize: 0.30, + // Minimum height + maxChildSize: 0.8, + // Maximum height + expand: false, + //Prevents full-screen takeover + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey500 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Select Payment Method".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Icon(Icons.close), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + controller: scrollController, + children: [ + Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + const SizedBox(height: 10), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + onPress: () async { + if (controller.selectedPaymentMethod.value.isEmpty) { + ShowToastDialog.showToast("Please select a payment method".tr); + } else { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.totalAmount.value.toString(), context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.completeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + if (Constant.userModel!.walletAmount == null || Constant.userModel!.walletAmount! < controller.totalAmount.value) { + ShowToastDialog.showToast("You do not have sufficient wallet balance".tr); + } else { + controller.completeOrder(); + } + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.totalAmount.value.toString()), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.totalAmount.value.toString(), orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + } + }, + ), + ], + ), + ); + }, + ); + } + + Obx cardDecoration(MyCabBookingController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: Constant.userModel!.walletAmount == null ? '0.0' : Constant.userModel!.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/ecommarce/all_brand_product_screen.dart b/lib/screen_ui/ecommarce/all_brand_product_screen.dart new file mode 100644 index 0000000..c50aa39 --- /dev/null +++ b/lib/screen_ui/ecommarce/all_brand_product_screen.dart @@ -0,0 +1,148 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/all_brand_product_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class AllBrandProductScreen extends StatelessWidget { + const AllBrandProductScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: AllBrandProductController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 20), + child: GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 3.5 / 6, crossAxisSpacing: 10), + padding: EdgeInsets.zero, + itemCount: controller.productList.length, + itemBuilder: (context, index) { + ProductModel productModel = controller.productList[index]; + return FutureBuilder( + future: FireStoreUtils.getVendorById(productModel.vendorID.toString()), + builder: (context, vendorSnapshot) { + if (!vendorSnapshot.hasData || vendorSnapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(); // Show placeholder or loader + } + VendorModel? vendorModel = vendorSnapshot.data; + String price = "0.0"; + String disPrice = "0.0"; + List selectedVariants = []; + List selectedIndexVariants = []; + List selectedIndexArray = []; + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + selectedVariants.add( + productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString(), + ); + selectedIndexVariants.add( + '${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}', + ); + selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + price = Constant.productCommissionPrice( + vendorModel!, + productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0', + ); + disPrice = "0"; + } + } else { + price = Constant.productCommissionPrice(vendorModel!, productModel.price.toString()); + disPrice = double.parse(productModel.disPrice.toString()) <= 0 ? "0" : Constant.productCommissionPrice(vendorModel, productModel.disPrice.toString()); + } + return GestureDetector( + onTap: () async { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 90, + width: Responsive.width(100, context), + child: NetworkImageWidget(imageUrl: productModel.photo.toString(), fit: BoxFit.cover), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + productModel.name!.capitalizeString(), + textAlign: TextAlign.start, + maxLines: 1, + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + disPrice == "" || disPrice == "0" + ? Text(Constant.amountShow(amount: price), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.primary300)) + : Row( + children: [ + Text( + Constant.amountShow(amount: price), + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: Colors.grey, decoration: TextDecoration.lineThrough), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: disPrice), + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.warning50 : AppThemeData.warning50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star, size: 18, color: AppThemeData.warning400), + Text( + "${Constant.calculateReview(reviewCount: productModel.reviewsCount.toString(), reviewSum: productModel.reviewsSum.toString())} (${productModel.reviewsSum})", + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: AppThemeData.warning400), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/ecommarce/all_category_product_screen.dart b/lib/screen_ui/ecommarce/all_category_product_screen.dart new file mode 100644 index 0000000..16d9658 --- /dev/null +++ b/lib/screen_ui/ecommarce/all_category_product_screen.dart @@ -0,0 +1,148 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/all_category_product_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class AllCategoryProductScreen extends StatelessWidget { + const AllCategoryProductScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: AllCategoryProductController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 3.5 / 6, crossAxisSpacing: 10), + padding: EdgeInsets.zero, + itemCount: controller.productList.length, + itemBuilder: (context, index) { + ProductModel productModel = controller.productList[index]; + return FutureBuilder( + future: FireStoreUtils.getVendorById(productModel.vendorID.toString()), + builder: (context, vendorSnapshot) { + if (!vendorSnapshot.hasData || vendorSnapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(); // Show placeholder or loader + } + VendorModel? vendorModel = vendorSnapshot.data; + String price = "0.0"; + String disPrice = "0.0"; + List selectedVariants = []; + List selectedIndexVariants = []; + List selectedIndexArray = []; + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + selectedVariants.add( + productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString(), + ); + selectedIndexVariants.add( + '${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}', + ); + selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + price = Constant.productCommissionPrice( + vendorModel!, + productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0', + ); + disPrice = "0"; + } + } else { + price = Constant.productCommissionPrice(vendorModel!, productModel.price.toString()); + disPrice = double.parse(productModel.disPrice.toString()) <= 0 ? "0" : Constant.productCommissionPrice(vendorModel, productModel.disPrice.toString()); + } + return GestureDetector( + onTap: () async { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 90, + width: Responsive.width(100, context), + child: NetworkImageWidget(imageUrl: productModel.photo.toString(), fit: BoxFit.cover), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + productModel.name!.capitalizeString(), + textAlign: TextAlign.start, + maxLines: 1, + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + disPrice == "" || disPrice == "0" + ? Text(Constant.amountShow(amount: price), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.primary300)) + : Row( + children: [ + Text( + Constant.amountShow(amount: price), + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: Colors.grey, decoration: TextDecoration.lineThrough), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: disPrice), + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.warning50 : AppThemeData.warning50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star, size: 18, color: AppThemeData.warning400), + Text( + "${Constant.calculateReview(reviewCount: productModel.reviewsCount.toString(), reviewSum: productModel.reviewsSum.toString())} (${productModel.reviewsSum})", + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: AppThemeData.warning400), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/ecommarce/dash_board_e_commerce_screen.dart b/lib/screen_ui/ecommarce/dash_board_e_commerce_screen.dart new file mode 100644 index 0000000..cc530cb --- /dev/null +++ b/lib/screen_ui/ecommarce/dash_board_e_commerce_screen.dart @@ -0,0 +1,84 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dash_board_controller.dart'; +import 'package:customer/controllers/dash_board_ecommarce_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../controllers/theme_controller.dart'; + +class DashBoardEcommerceScreen extends StatelessWidget { + const DashBoardEcommerceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: DashBoardEcommerceController(), + builder: (controller) { + return Scaffold( + body: controller.pageList[controller.selectedIndex.value], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + showUnselectedLabels: true, + showSelectedLabels: true, + selectedFontSize: 12, + selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + currentIndex: controller.selectedIndex.value, + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + selectedItemColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + unselectedItemColor: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + onTap: (int index) { + if (index == 0) { + Get.put(DashBoardController()); + } + controller.selectedIndex.value = index; + }, + items: + Constant.walletSetting == false + ? [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_orders.svg", label: 'Orders'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ] + : [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_wallet.svg", label: 'Wallet'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_orders.svg", label: 'Orders'.tr, controller: controller), + navigationBarItem(isDark, index: 4, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ], + ), + ); + }, + ); + }); + } + + BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required DashBoardEcommerceController controller}) { + return BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: SvgPicture.asset( + assetIcon, + height: 22, + width: 22, + color: + controller.selectedIndex.value == index + ? isDark + ? AppThemeData.primary300 + : AppThemeData.primary300 + : isDark + ? AppThemeData.grey300 + : AppThemeData.grey600, + ), + ), + label: label, + ); + } +} diff --git a/lib/screen_ui/ecommarce/home_e_commerce_screen.dart b/lib/screen_ui/ecommarce/home_e_commerce_screen.dart new file mode 100644 index 0000000..914beb2 --- /dev/null +++ b/lib/screen_ui/ecommarce/home_e_commerce_screen.dart @@ -0,0 +1,1131 @@ +import 'package:badges/badges.dart' as badges; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/home_e_commerce_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/advertisement_model.dart'; +import 'package:customer/models/banner_model.dart'; +import 'package:customer/models/brands_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/screen_ui/ecommarce/all_brand_product_screen.dart'; +import 'package:customer/screen_ui/ecommarce/all_category_product_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/advertisement_screens/all_advertisement_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/cart_screen/cart_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/category_restaurant_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/restaurant_list_screen.dart' show RestaurantListScreen; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/view_all_category_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/search_screen/search_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_border.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/widget/osm_map/map_picker_page.dart'; +import 'package:customer/widget/place_picker/location_picker_screen.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:customer/widget/video_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class HomeECommerceScreen extends StatelessWidget { + const HomeECommerceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: HomeECommerceController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + titleSpacing: 0, + leading: InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.arrow_back, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, size: 20), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell( + onTap: () { + Get.offAll(const LoginScreen()); + }, + child: Text("Login".tr, textAlign: TextAlign.center, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontSize: 12)), + ) + : Text(Constant.userModel!.fullName(), textAlign: TextAlign.center, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontSize: 12)), + InkWell( + onTap: () async { + if (Constant.userModel != null) { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } else { + Constant.checkPermission( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + // ✅ declare it once here! + ShippingAddress shippingAddress = ShippingAddress(); + + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(); + ShowToastDialog.closeLoader(); + + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + shippingAddress.addressAs = "Home"; + shippingAddress.locality = address.toString(); + shippingAddress.location = UserLocation(latitude: lat, longitude: lng); + Constant.selectedLocation = shippingAddress; + controller.getData(); + Get.back(); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + shippingAddress.locality = "Picked from Map"; // You can reverse-geocode + + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + shippingAddress.locality = currentLocation; + }); + + Constant.selectedLocation = shippingAddress; + ShowToastDialog.closeLoader(); + controller.getData(); + } + }, + context: context, + ); + } + }, + child: Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + TextSpan(text: Constant.selectedLocation.getFullAddress(), style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontSize: 14)), + WidgetSpan(child: SvgPicture.asset("assets/icons/ic_down.svg", colorFilter: ColorFilter.mode(AppThemeData.grey50, BlendMode.srcIn))), + ], + ), + ), + ), + ], + ), + actions: [ + Obx( + () => Padding( + padding: const EdgeInsets.only(right: 15.0, left: 10), + child: badges.Badge( + showBadge: true, + badgeContent: Text( + "${cartItem.length}", + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + badgeStyle: badges.BadgeStyle(shape: badges.BadgeShape.circle, badgeColor: AppThemeData.info300), + child: InkWell( + onTap: () async { + (await Get.to(const CartScreen())); + controller.getCartData(); + }, + child: ClipOval( + child: Container( + width: 30, + height: 30, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset("assets/icons/ic_shoping_cart.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey50 : AppThemeData.grey50, BlendMode.srcIn)), + ), + ), + ), + ), + ), + ), + ), + ], + bottom: PreferredSize( + preferredSize: Size.fromHeight(50.0), // height of the bottom widget + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: InkWell( + onTap: () { + Get.to(const SearchScreen(), arguments: {"vendorList": controller.allNearestRestaurant}); + }, + child: TextFieldWidget( + hintText: 'Search the store, item and more...'.tr, + controller: null, + enable: false, + backgroundColor: AppThemeData.grey50, + hintColor: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + prefix: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SvgPicture.asset("assets/icons/ic_search.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey400 : AppThemeData.grey400, BlendMode.srcIn)), + ), + ), + ), + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Text( + "Category".tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16), + ), + ), + InkWell( + onTap: () { + Get.to(const ViewAllCategoryScreen()); + }, + child: Text( + "View all".tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle( + decoration: TextDecoration.underline, + color: isDark ? AppThemeData.multiVendorDark300 : AppThemeData.multiVendor300, + fontSize: 14, + ), + ), + ), + ], + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 100, + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.vendorCategoryModel.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryModel[index]; + return InkWell( + onTap: () { + Get.to(const CategoryRestaurantScreen(), arguments: {"vendorCategoryModel": vendorCategoryModel, "dineIn": false}); + }, + child: Padding( + padding: const EdgeInsets.only(right: 18), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + NetworkImageWidget(imageUrl: vendorCategoryModel.photo.toString(), height: 60, width: 60, fit: BoxFit.cover), + const SizedBox(height: 5), + Text( + vendorCategoryModel.title.toString(), + textAlign: TextAlign.center, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800, fontSize: 14), + ), + ], + ), + ), + ); + }, + ), + ), + ), + SizedBox(height: 10), + Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: controller.bannerModel.isEmpty ? const SizedBox() : BannerView(controller: controller)), + Visibility(visible: (Constant.isEnableAdsFeature == true && controller.advertisementList.isNotEmpty), child: const SizedBox(height: 20)), + Visibility( + visible: Constant.isEnableAdsFeature == true, + child: + controller.advertisementList.isEmpty + ? const SizedBox() + : Container( + color: AppThemeData.primary300.withAlpha(40), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "Highlights for you".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + InkWell( + onTap: () { + Get.to(AllAdvertisementScreen())?.then((value) { + controller.getFavouriteRestaurant(); + }); + }, + child: Text( + "View all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 220, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: controller.advertisementList.length >= 10 ? 10 : controller.advertisementList.length, + padding: EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return AdvertisementHomeCard(controller: controller, model: controller.advertisementList[index]); + }, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "New Arrivals".tr, + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16), + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 380, + child: GridView.count( + crossAxisCount: 2, + // 2 columns + mainAxisSpacing: 0, + crossAxisSpacing: 20, + childAspectRatio: 1 / 1.1, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + children: controller.newArrivalRestaurantList.take(4).map((item) => NewArrivalCard(item: item)).toList(), + ), + ), + ), + SizedBox(height: 5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: RoundedButtonBorder( + radius: 10, + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + title: 'View All Arrivals'.tr, + onPress: () { + Get.to(RestaurantListScreen(), arguments: {"vendorList": controller.newArrivalRestaurantList, "title": "New Arrivals".tr}); + }, + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Top Brands".tr, textAlign: TextAlign.start, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16)), + SizedBox(height: 10), + GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4, childAspectRatio: 4.5 / 6, crossAxisSpacing: 2), + itemCount: controller.brandList.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + BrandsModel brandModel = controller.brandList[index]; + return InkWell( + onTap: () { + Get.to(AllBrandProductScreen(), arguments: {"brandModel": brandModel}); + }, + child: Column( + children: [ + Container( + width: 80, + height: 80, + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, strokeAlign: BorderSide.strokeAlignOutside, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), + borderRadius: BorderRadius.circular(10), + ), + ), + child: Padding(padding: const EdgeInsets.all(10), child: ClipOval(child: NetworkImageWidget(imageUrl: brandModel.photo.toString(), fit: BoxFit.cover))), + ), + SizedBox(height: 5), + Text( + '${brandModel.title}', + textAlign: TextAlign.center, + maxLines: 2, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ); + }, + ), + ], + ), + ), + SizedBox(height: 10), + ListView.builder( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: controller.categoryWiseProductList.length, + itemBuilder: (context, index) { + VendorCategoryModel item = controller.categoryWiseProductList[index]; + String imagePath = ["assets/images/ic_product_bg_1.png", "assets/images/ic_product_bg_2.png", "assets/images/ic_product_bg_3.png"][index % ["", "", ""].length]; + return Container( + width: Responsive.width(100, context), + decoration: BoxDecoration(image: DecorationImage(image: AssetImage(imagePath), fit: BoxFit.fill)), + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 10, bottom: 20), + child: FutureBuilder>( + future: FireStoreUtils.getProductListByCategoryId(item.id.toString()), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))); + } else if ((snapshot.hasData || (snapshot.data?.isNotEmpty ?? false))) { + List productList = snapshot.data!; + return snapshot.data!.isEmpty + ? Container() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(item.title.toString(), textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 18)), + Text( + "Style up with the latest fits, now at unbeatable prices.".tr, + textAlign: TextAlign.start, + style: AppThemeData.regularTextStyle(color: AppThemeData.grey900, fontSize: 12), + ), + SizedBox(height: 20), + GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 3.5 / 6, crossAxisSpacing: 10), + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: productList.length > 6 ? 6 : productList.length, + itemBuilder: (context, index) { + ProductModel productModel = productList[index]; + return FutureBuilder( + future: FireStoreUtils.getVendorById(productModel.vendorID.toString()), + builder: (context, vendorSnapshot) { + if (!vendorSnapshot.hasData || vendorSnapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(); // Show placeholder or loader + } + VendorModel? vendorModel = vendorSnapshot.data; + String price = "0.0"; + String disPrice = "0.0"; + List selectedVariants = []; + List selectedIndexVariants = []; + List selectedIndexArray = []; + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + selectedVariants.add( + productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString(), + ); + selectedIndexVariants.add( + '${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}', + ); + selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + price = Constant.productCommissionPrice( + vendorModel!, + productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0', + ); + disPrice = "0"; + } + } else { + price = Constant.productCommissionPrice(vendorModel!, productModel.price.toString()); + disPrice = + double.parse(productModel.disPrice.toString()) <= 0 + ? "0" + : Constant.productCommissionPrice(vendorModel, productModel.disPrice.toString()); + } + return GestureDetector( + onTap: () async { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 90, + width: Responsive.width(100, context), + child: NetworkImageWidget(imageUrl: productModel.photo.toString(), fit: BoxFit.cover), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + productModel.name!.capitalizeString(), + textAlign: TextAlign.start, + maxLines: 1, + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + disPrice == "" || disPrice == "0" + ? Text(Constant.amountShow(amount: price), style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.primary300)) + : Column( + children: [ + Text( + Constant.amountShow(amount: price), + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: Colors.grey, decoration: TextDecoration.lineThrough), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: disPrice), + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.warning50 : AppThemeData.warning50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star, size: 18, color: AppThemeData.warning400), + Text( + "${Constant.calculateReview(reviewCount: productModel.reviewsCount.toString(), reviewSum: productModel.reviewsSum.toString())} (${productModel.reviewsSum})", + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: AppThemeData.warning400), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ), + RoundedButtonBorder( + radius: 10, + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + title: 'View All Products', + onPress: () { + Get.to(AllCategoryProductScreen(), arguments: {"categoryModel": item}); + }, + ), + ], + ); + } else { + return SizedBox(); + } + }, + ), + ), + ); + }, + ), + SizedBox(height: 10), + Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: controller.bannerModel.isEmpty ? const SizedBox() : BannerBottomView(controller: controller)), + // Visibility( + // visible: (Constant.isEnableAdsFeature == true && controller.advertisementList.isNotEmpty), + // child: const SizedBox(height: 20), + // ), + // Visibility( + // visible: Constant.isEnableAdsFeature == true, + // child: + // controller.advertisementList.isEmpty + // ? const SizedBox() + // : Container( + // color: AppThemeData.primary300.withAlpha(40), + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // Expanded( + // child: Text( + // "Highlights for you".tr, + // textAlign: TextAlign.start, + // style: TextStyle( + // fontFamily: AppThemeData.semiBold, + // fontSize: 16, + // color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + // ), + // ), + // ), + // InkWell( + // onTap: () { + // Get.to(AllAdvertisementScreen())?.then((value) { + // controller.getFavouriteRestaurant(); + // }); + // }, + // child: Text( + // "View all".tr, + // textAlign: TextAlign.center, + // style: TextStyle( + // fontFamily: AppThemeData.regular, + // color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + // ), + // ), + // ), + // ], + // ), + // const SizedBox(height: 16), + // SizedBox( + // height: 220, + // child: ListView.builder( + // physics: const BouncingScrollPhysics(), + // scrollDirection: Axis.horizontal, + // itemCount: + // controller.advertisementList.length >= 10 + // ? 10 + // : controller.advertisementList.length, + // padding: EdgeInsets.all(0), + // itemBuilder: (BuildContext context, int index) { + // return AdvertisementHomeCard( + // controller: controller, + // model: controller.advertisementList[index], + // ); + // }, + // ), + // ), + // ], + // ), + // ), + // ), + // ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("All Store".tr, textAlign: TextAlign.start, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16)), + SizedBox(height: 10), + ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.allNearestRestaurant.length > 8 ? 8 : controller.allNearestRestaurant.length, + itemBuilder: (context, index) { + VendorModel item = controller.allNearestRestaurant[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": item}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + children: [ + ClipRRect(borderRadius: BorderRadius.circular(10), child: NetworkImageWidget(imageUrl: item.photo.toString(), height: 80, width: 130, fit: BoxFit.cover)), + SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.title.toString(), style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16)), + Row( + children: [ + Icon(Icons.location_on, size: 14, color: Colors.grey), + SizedBox(width: 4), + Expanded( + child: Text( + item.location.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.warning50 : AppThemeData.warning50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star, size: 18, color: AppThemeData.warning400), + Text( + "${Constant.calculateReview(reviewCount: item.reviewsCount.toString(), reviewSum: item.reviewsSum.toString())} (${item.reviewsSum})", + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: AppThemeData.warning400), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + RoundedButtonBorder( + radius: 10, + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + title: 'View All Stores'.tr, + onPress: () { + Get.to(const RestaurantListScreen(), arguments: {"vendorList": controller.allNearestRestaurant}); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class NewArrivalCard extends StatelessWidget { + final VendorModel item; + + const NewArrivalCard({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": item}); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget( + height: 100, + width: double.infinity, + fit: BoxFit.cover, + imageUrl: item.photo != null && item.photo!.isNotEmpty ? item.photo.toString() : Constant.placeHolderImage.toString(), + ), + ), + SizedBox(height: 5), + Text(item.title.toString(), style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 14)), + Row( + children: [ + Icon(Icons.location_on, size: 14, color: Colors.grey), + SizedBox(width: 4), + Expanded( + child: Text( + item.location.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.warning50 : AppThemeData.warning50, borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.star, size: 18, color: AppThemeData.warning400), + Text( + "${Constant.calculateReview(reviewCount: item.reviewsCount.toString(), reviewSum: item.reviewsSum.toString())} (${item.reviewsSum})", + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: AppThemeData.warning400), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class BannerView extends StatelessWidget { + final HomeECommerceController controller; + + const BannerView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 160, + child: PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.bannerModel.length, + padEnds: false, + pageSnapping: true, + allowImplicitScrolling: true, + onPageChanged: (value) { + controller.currentPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + BannerModel bannerModel = controller.bannerModel[index]; + return InkWell( + onTap: () async { + if (bannerModel.redirect_type == "store") { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(bannerModel.redirect_id.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "product") { + ShowToastDialog.showLoader("Please wait...".tr); + ProductModel? productModel = await FireStoreUtils.getProductById(bannerModel.redirect_id.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel!.vendorID.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "external_link") { + final uri = Uri.parse(bannerModel.redirect_id.toString()); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + ShowToastDialog.showToast("Could not launch".tr); + } + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 14), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(12)), child: NetworkImageWidget(imageUrl: bannerModel.photo.toString(), fit: BoxFit.cover)), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.bannerModel.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentPage.value == index ? AppThemeData.primary300 : Colors.black12), + ), + ); + }), + ), + ), + ], + ); + } +} + +class BannerBottomView extends StatelessWidget { + final HomeECommerceController controller; + + const BannerBottomView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 150, + child: PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageBottomController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.bannerBottomModel.length, + padEnds: false, + pageSnapping: true, + allowImplicitScrolling: true, + onPageChanged: (value) { + controller.currentBottomPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + BannerModel bannerModel = controller.bannerBottomModel[index]; + return InkWell( + onTap: () async { + if (bannerModel.redirect_type == "store") { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(bannerModel.redirect_id.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "product") { + ShowToastDialog.showLoader("Please wait...".tr); + ProductModel? productModel = await FireStoreUtils.getProductById(bannerModel.redirect_id.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel!.vendorID.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "external_link") { + final uri = Uri.parse(bannerModel.redirect_id.toString()); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + ShowToastDialog.showToast("Could not launch".tr); + } + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 14), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(12)), child: NetworkImageWidget(imageUrl: bannerModel.photo.toString(), fit: BoxFit.cover)), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.bannerBottomModel.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentBottomPage.value == index ? AppThemeData.primary300 : Colors.black12), + ), + ); + }), + ), + ), + ], + ); + } +} + +class AdvertisementHomeCard extends StatelessWidget { + final AdvertisementModel model; + final HomeECommerceController controller; + + const AdvertisementHomeCard({super.key, required this.controller, required this.model}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(model.vendorId!); + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Container( + margin: EdgeInsets.only(right: 16), + width: Responsive.width(70, context), + decoration: BoxDecoration( + color: isDark ? AppThemeData.info600 : AppThemeData.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: isDark ? 6 : 2, spreadRadius: 0, offset: Offset(0, isDark ? 3 : 1))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + model.type == 'restaurant_promotion' + ? ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + child: NetworkImageWidget(imageUrl: model.coverImage ?? '', height: 135, width: double.infinity, fit: BoxFit.cover), + ) + : VideoAdvWidget(url: model.video ?? '', height: 135, width: double.infinity), + if (model.type != 'video_promotion' && model.vendorId != null && (model.showRating == true || model.showReview == true)) + Positioned( + bottom: 8, + right: 8, + child: FutureBuilder( + future: FireStoreUtils.getVendorById(model.vendorId!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(); + } else { + if (snapshot.hasError) { + return const SizedBox(); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + VendorModel vendorModel = snapshot.data!; + return Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + if (model.showRating == true) SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + if (model.showRating == true) const SizedBox(width: 5), + Text( + "${model.showRating == true ? Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString()) : ''} ${model.showReview == true ? '(${vendorModel.reviewsCount!.toStringAsFixed(0)})' : ''}", + style: TextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + } + }, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (model.type == 'restaurant_promotion') + ClipRRect(borderRadius: BorderRadius.circular(30), child: NetworkImageWidget(imageUrl: model.profileImage ?? '', height: 50, width: 50, fit: BoxFit.cover)), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.title ?? '', + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 14, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + Text( + model.description ?? '', + style: TextStyle(fontSize: 12, fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey400 : AppThemeData.grey600), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ), + ), + model.type == 'restaurant_promotion' + ? IconButton( + icon: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == model.vendorId).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey400 : AppThemeData.grey600, BlendMode.srcIn)), + ), + onPressed: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == model.vendorId).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: model.vendorId, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == model.vendorId); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: model.vendorId, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + controller.update(); + }, + ) + : Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), + child: Padding(padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: Icon(Icons.arrow_forward, size: 20, color: AppThemeData.primary300)), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/location_enable_screens/address_list_screen.dart b/lib/screen_ui/location_enable_screens/address_list_screen.dart new file mode 100644 index 0000000..2584ba2 --- /dev/null +++ b/lib/screen_ui/location_enable_screens/address_list_screen.dart @@ -0,0 +1,149 @@ +import 'package:customer/constant/assets.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/address_list_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/location_enable_screens/enter_manually_location.dart'; +import 'package:customer/themes/app_them_data.dart' show AppThemeData; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +class AddressListScreen extends StatelessWidget { + const AddressListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: AddressListController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + leading: InkWell( + onTap: () { + Get.back(); + }, + child: const Icon(Icons.arrow_back, size: 24, color: Colors.grey), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("My Addresses".tr, style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 5), + Text("Allows users to view, manage, add, or edit delivery addresses.".tr, style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.grey600)), + const SizedBox(height: 24), + Expanded( + child: + controller.shippingAddressList.isEmpty + ? Constant.showEmptyView(message: "Address not found".tr) + : ListView.separated( + itemCount: controller.shippingAddressList.length, + itemBuilder: (context, index) { + ShippingAddress address = controller.shippingAddressList[index]; + return InkWell( + onTap: () { + Get.back(result: address); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Text( + address.addressAs.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ), + ), + SizedBox(width: 10), + address.isDefault == true + ? Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.success100 : AppThemeData.success100, borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Text( + "Default".tr, + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ), + ) + : SizedBox(), + ], + ), + SizedBox(height: 10), + Text( + address.getFullAddress().toString(), + style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + ), + ], + ), + ), + InkWell( + onTap: () async { + await controller.deleteAddress(index); + }, + child: SvgPicture.asset("assets/icons/ic_delete_address.svg"), + ), + SizedBox(width: 10), + InkWell( + onTap: () { + Get.to(EnterManuallyLocationScreen(), arguments: {"address": address, "mode": "Edit"})!.then((value) { + if (value == true) { + controller.getUser(); + } + }); + }, + child: SvgPicture.asset("assets/icons/ic_edit_address.svg"), + ), + ], + ), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Padding(padding: const EdgeInsets.symmetric(vertical: 20), child: Divider(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, height: 1)); + }, + ), + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(bottom: 30, left: 16, right: 16, top: 20), + child: RoundedButtonFill( + title: "Add New Address", + onPress: () { + Get.to(EnterManuallyLocationScreen())!.then((value) { + if (value == true) { + controller.getUser(); + } + }); + }, + isRight: false, + isCenter: true, + icon: SvgPicture.asset(AppAssets.icPlus, width: 20, height: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.greyDark900), + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + textColor: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/location_enable_screens/enter_manually_location.dart b/lib/screen_ui/location_enable_screens/enter_manually_location.dart new file mode 100644 index 0000000..f53d8ee --- /dev/null +++ b/lib/screen_ui/location_enable_screens/enter_manually_location.dart @@ -0,0 +1,238 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/utils.dart'; +import 'package:customer/widget/osm_map/map_picker_page.dart'; +import 'package:customer/widget/place_picker/location_picker_screen.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../controllers/enter_manually_location_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; + +class EnterManuallyLocationScreen extends StatelessWidget { + const EnterManuallyLocationScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: EnterManuallyLocationController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + leading: InkWell( + onTap: () { + Get.back(); + }, + child: const Icon(Icons.arrow_back, size: 24, color: Colors.grey), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.mode == "Edit" ? "Edit Address".tr : "Add a New Address".tr, + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + Text("Enter your location details so we can deliver your orders quickly and accurately.".tr, style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.grey600)), + const SizedBox(height: 24), + Row( + children: [ + Expanded(child: Text("Set as Default Address".tr, style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.grey600))), + Transform.scale( + scale: 0.7, // Decrease the size (try 0.5, 0.6, etc.) + child: Switch( + value: controller.isDefault.value, + onChanged: (value) { + controller.isDefault.value = value; + }, + activeThumbColor: Colors.green, + inactiveThumbColor: Colors.grey, + ), + ), + ], + ), + GestureDetector( + onTap: () { + Constant.checkPermission( + context: context, + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + controller.localityEditingController.value.text = address.toString(); + controller.location.value = UserLocation(latitude: lat, longitude: lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.localityEditingController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.location.value = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + } + }); + } + }, + ); + }, + child: TextFieldWidget( + title: "Choose Location".tr, + hintText: "Choose Location".tr, + readOnly: true, + enable: false, + controller: null, + suffix: GestureDetector( + onTap: () { + Constant.checkPermission( + context: context, + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + controller.localityEditingController.value.text = address.toString(); + controller.location.value = UserLocation(latitude: lat, longitude: lng); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + controller.localityEditingController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.location.value = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + Get.back(); + } + }); + } + }, + ); + }, + child: Padding(padding: const EdgeInsets.only(right: 10), child: Icon(Icons.gps_fixed, size: 24, color: AppThemeData.ecommerce300)), + ), + ), + ), + const SizedBox(height: 15), + TextFieldWidget(title: "Flat/House/Floor/Building*".tr, hintText: "Enter address details".tr, controller: controller.houseBuildingTextEditingController.value), + const SizedBox(height: 15), + TextFieldWidget(title: "Area/Sector/Locality*".tr, hintText: "Enter area/locality".tr, controller: controller.localityEditingController.value), + const SizedBox(height: 15), + TextFieldWidget(title: "Nearby Landmark".tr, hintText: "Add a landmark".tr, controller: controller.landmarkEditingController.value), + const SizedBox(height: 30), + Container(height: 1, color: AppThemeData.grey200), + const SizedBox(height: 25), + Text("Save Address As".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900)), + const SizedBox(height: 10), + Wrap( + spacing: 10, + children: + controller.saveAsList + .map( + (item) => GestureDetector( + onTap: () { + controller.selectedSaveAs.value = item; + }, + child: Container( + decoration: BoxDecoration( + color: controller.selectedSaveAs.value == item ? AppThemeData.primary300 : AppThemeData.grey100, + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Text( + controller.getLocalizedSaveAs(item), + style: AppThemeData.mediumTextStyle(color: controller.selectedSaveAs.value == item ? AppThemeData.grey50 : AppThemeData.grey600), + textAlign: TextAlign.center, + ), + ), + ), + ) + .toList(), + ), + const SizedBox(height: 30), + RoundedButtonFill( + title: "Save Address".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + if (controller.location.value.latitude == null || controller.location.value.longitude == null) { + ShowToastDialog.showToast("Please select Location".tr); + } else if (controller.houseBuildingTextEditingController.value.text.isEmpty) { + ShowToastDialog.showToast("Please Enter Flat / House / Floor / Building".tr); + } else if (controller.localityEditingController.value.text.isEmpty) { + ShowToastDialog.showToast("Please Enter Area / Sector / Locality".tr); + } else { + ShowToastDialog.showLoader("Please wait...".tr); + + //Common values + controller.shippingModel.value.location = controller.location.value; + controller.shippingModel.value.addressAs = controller.selectedSaveAs.value; + controller.shippingModel.value.address = controller.houseBuildingTextEditingController.value.text; + controller.shippingModel.value.locality = controller.localityEditingController.value.text; + controller.shippingModel.value.landmark = controller.landmarkEditingController.value.text; + + if (controller.mode.value == "Edit") { + //Edit Mode + controller.shippingAddressList.value = + controller.shippingAddressList.map((address) { + if (address.id == controller.shippingModel.value.id) { + return controller.shippingModel.value; // replace existing one + } + return address; + }).toList(); + Constant.selectedLocation = controller.shippingModel.value; + } else { + //Add Mode + controller.shippingModel.value.id = Constant.getUuid(); + controller.shippingModel.value.isDefault = controller.shippingAddressList.isEmpty ? true : false; + controller.shippingAddressList.add(controller.shippingModel.value); + } + + //Handle default address switch + if (controller.isDefault.value) { + controller.shippingAddressList.value = + controller.shippingAddressList.map((address) { + address.isDefault = address.id == controller.shippingModel.value.id ? true : false; + return address; + }).toList(); + } + + controller.userModel.value.shippingAddress = controller.shippingAddressList; + await FireStoreUtils.updateUser(controller.userModel.value); + + ShowToastDialog.closeLoader(); + Get.back(result: true); + } + }, + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/location_enable_screens/location_permission_screen.dart b/lib/screen_ui/location_enable_screens/location_permission_screen.dart new file mode 100644 index 0000000..0e0f069 --- /dev/null +++ b/lib/screen_ui/location_enable_screens/location_permission_screen.dart @@ -0,0 +1,189 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart'; +import 'package:customer/screen_ui/service_home_screen/service_list_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/widget/osm_map/map_picker_page.dart'; +import 'package:customer/widget/place_picker/location_picker_screen.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; + +import '../../constant/assets.dart'; +import '../../utils/utils.dart'; + +class LocationPermissionScreen extends StatelessWidget { + const LocationPermissionScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 20), + Image.asset(AppAssets.icLocation), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Text( + "Enable Location for a Personalized Experience".tr, + style: AppThemeData.boldTextStyle(fontSize: 24, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 50), + child: Text( + "Allow location access to discover beauty stores and services near you.".tr, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 30), + RoundedButtonFill( + title: "Use current location".tr, + onPress: () async { + Constant.checkPermission( + context: context, + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + ShippingAddress addressModel = ShippingAddress(); + try { + await Geolocator.requestPermission(); + Position newLocalData = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); + await placemarkFromCoordinates(newLocalData.latitude, newLocalData.longitude).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + addressModel.addressAs = "Home"; + addressModel.location = UserLocation(latitude: newLocalData.latitude, longitude: newLocalData.longitude); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + addressModel.locality = currentLocation; + }); + + Constant.selectedLocation = addressModel; + Constant.currentLocation = await Utils.getCurrentLocation(); + + ShowToastDialog.closeLoader(); + + Get.offAll(const ServiceListScreen()); + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + addressModel.addressAs = "Home"; + addressModel.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + addressModel.locality = currentLocation; + }); + + Constant.selectedLocation = addressModel; + Constant.currentLocation = await Utils.getCurrentLocation(); + + ShowToastDialog.closeLoader(); + + Get.offAll(const ServiceListScreen()); + } + }, + ); + }, + color: AppThemeData.grey900, + textColor: AppThemeData.grey50, + ), + const SizedBox(height: 10), + RoundedButtonFill( + title: "Set from map".tr, + onPress: () async { + Constant.checkPermission( + context: context, + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + ShippingAddress addressModel = ShippingAddress(); + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); + ShowToastDialog.closeLoader(); + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + addressModel.addressAs = "Home"; + addressModel.locality = address.toString(); + addressModel.location = UserLocation(latitude: lat, longitude: lng); + Constant.selectedLocation = addressModel; + Get.offAll(const ServiceListScreen()); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + addressModel.addressAs = "Home"; + addressModel.locality = Utils.formatAddress(selectedLocation: selectedLocationModel); + addressModel.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + Constant.selectedLocation = addressModel; + + Get.offAll(const ServiceListScreen()); + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + addressModel.addressAs = "Home"; + addressModel.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + addressModel.locality = currentLocation; + }); + + Constant.selectedLocation = addressModel; + ShowToastDialog.closeLoader(); + + Get.offAll(const ServiceListScreen()); + } + }, + ); + }, + color: AppThemeData.grey50, + textColor: AppThemeData.grey900, + ), + const SizedBox(height: 20), + Constant.userModel == null + ? const SizedBox() + : GestureDetector( + onTap: () async { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress addressModel = value; + Constant.selectedLocation = addressModel; + Get.offAll(const ServiceListScreen()); + } + }); + }, + child: Text("Enter Manually location".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/maintenance_mode_screen/maintenance_mode_screen.dart b/lib/screen_ui/maintenance_mode_screen/maintenance_mode_screen.dart new file mode 100644 index 0000000..e95bc4f --- /dev/null +++ b/lib/screen_ui/maintenance_mode_screen/maintenance_mode_screen.dart @@ -0,0 +1,32 @@ +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class MaintenanceModeScreen extends StatelessWidget { + const MaintenanceModeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppThemeData.grey100, + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center(child: Image.asset('assets/images/maintenance.png', height: 200, width: 200)), + const SizedBox(height: 20), + Text("We'll be back soon!".tr, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + "Sorry for the inconvenience but we're performing some maintenance at the moment. We'll be back online shortly!".tr, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/advertisement_screens/all_advertisement_screen.dart b/lib/screen_ui/multi_vendor_service/advertisement_screens/all_advertisement_screen.dart new file mode 100644 index 0000000..ce39b54 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/advertisement_screens/all_advertisement_screen.dart @@ -0,0 +1,196 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/advertisement_list_controller.dart'; +import 'package:customer/models/advertisement_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/video_widget.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class AllAdvertisementScreen extends StatelessWidget { + const AllAdvertisementScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: AdvertisementListController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + "Highlights for you".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : controller.advertisementList.isEmpty + ? Constant.showEmptyView(message: "Highlights for you not found.".tr) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: controller.advertisementList.length, + padding: EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return AdvertisementCard(controller: controller, model: controller.advertisementList[index]); + }, + ), + ), + ); + }, + ); + } +} + +class AdvertisementCard extends StatelessWidget { + final AdvertisementModel model; + final AdvertisementListController controller; + + const AdvertisementCard({super.key, required this.controller, required this.model}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(model.vendorId!); + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Container( + margin: EdgeInsets.only(bottom: 16), + width: Responsive.width(80, context), + decoration: BoxDecoration( + color: isDark ? AppThemeData.info600 : AppThemeData.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: isDark ? 6 : 2, spreadRadius: 0, offset: Offset(0, isDark ? 3 : 1))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + model.type == 'restaurant_promotion' + ? ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + child: NetworkImageWidget(imageUrl: model.coverImage ?? '', height: 150, width: double.infinity, fit: BoxFit.cover), + ) + : VideoAdvWidget(url: model.video ?? '', height: 150, width: double.infinity), + if (model.type != 'video_promotion' && model.vendorId != null && (model.showRating == true || model.showReview == true)) + Positioned( + bottom: 8, + right: 8, + child: FutureBuilder( + future: FireStoreUtils.getVendorById(model.vendorId!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(); + } else { + if (snapshot.hasError) { + return const SizedBox(); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + VendorModel vendorModel = snapshot.data!; + return Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + if (model.showRating == true) SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + if (model.showRating == true) const SizedBox(width: 5), + Text( + "${model.showRating == true ? Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString()) : ''}${model.showRating == true && model.showReview == true ? ' ' : ''}${model.showReview == true ? '(${vendorModel.reviewsCount!.toStringAsFixed(0)})' : ''}", + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + } + }, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (model.type == 'restaurant_promotion') + ClipRRect(borderRadius: BorderRadius.circular(30), child: NetworkImageWidget(imageUrl: model.profileImage ?? '', height: 50, width: 50, fit: BoxFit.cover)), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.title ?? '', + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + Text( + model.description ?? '', + style: TextStyle(fontSize: 14, fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey400 : AppThemeData.grey600), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ), + ), + model.type == 'restaurant_promotion' + ? Obx( + () => IconButton( + icon: + controller.favouriteList.where((p0) => p0.restaurantId == model.vendorId).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey400 : AppThemeData.grey600, BlendMode.srcIn)), + onPressed: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == model.vendorId).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: model.vendorId, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == model.vendorId); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: model.vendorId, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + ), + ) + : Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), + child: Padding(padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: Icon(Icons.arrow_forward, size: 20, color: AppThemeData.primary300)), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/cart_screen/cart_screen.dart b/lib/screen_ui/multi_vendor_service/cart_screen/cart_screen.dart new file mode 100644 index 0000000..03a0869 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/cart_screen/cart_screen.dart @@ -0,0 +1,1212 @@ +import 'package:bottom_picker/bottom_picker.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cart_controller.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/cart_screen/select_payment_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../models/user_model.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/my_separator.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; +import '../wallet_screen/wallet_screen.dart'; +import 'coupon_list_screen.dart'; + +class CartScreen extends StatelessWidget { + const CartScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CartController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface), + body: + cartItem.isEmpty + ? Constant.showEmptyView(message: "Item Not available".tr) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + controller.selectedFoodType.value == 'TakeAway' + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: InkWell( + onTap: () { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + if (Constant.checkZoneCheck(shippingAddress.location!.latitude ?? 0.0, shippingAddress.location!.longitude ?? 0.0)) { + controller.selectedAddress.value = shippingAddress; + controller.calculatePrice(); + } else { + ShowToastDialog.showToast("Service not available in this area".tr); + } + } + }); + }, + child: Column( + children: [ + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_send_one.svg"), + const SizedBox(width: 10), + Expanded( + child: Text( + controller.selectedAddress.value.addressAs.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + ), + SvgPicture.asset("assets/icons/ic_down.svg"), + ], + ), + const SizedBox(height: 5), + Text( + controller.selectedAddress.value.getFullAddress(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: cartItem.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + CartProductModel cartProductModel = cartItem[index]; + ProductModel? productModel; + FireStoreUtils.getProductById(cartProductModel.id!.split('~').first).then((value) { + productModel = value; + }); + print("cartItem[index] :: ${cartItem[index].extras} ::${cartItem[index].extrasPrice}"); + return InkWell( + onTap: () async { + await FireStoreUtils.getVendorById(productModel!.vendorID.toString()).then((value) { + if (value != null) { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": value}); + } + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: NetworkImageWidget( + imageUrl: cartProductModel.photo.toString(), + height: Responsive.height(10, context), + width: Responsive.width(20, context), + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${cartProductModel.name}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + double.parse(cartProductModel.discountPrice.toString()) <= 0 + ? Text( + Constant.amountShow(amount: cartProductModel.price), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ) + : Row( + children: [ + Text( + Constant.amountShow(amount: cartProductModel.discountPrice.toString()), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: cartProductModel.price), + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.lineThrough, + decorationColor: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + color: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFD1D5DB)), borderRadius: BorderRadius.circular(200)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () { + controller.addToCart(cartProductModel: cartProductModel, isIncrement: false, quantity: cartProductModel.quantity! - 1); + }, + child: Icon(Icons.remove, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + cartProductModel.quantity.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + InkWell( + onTap: () { + if (productModel!.itemAttribute != null) { + if (productModel!.itemAttribute!.variants!.where((element) => element.variantSku == cartProductModel.variantInfo!.variantSku).isNotEmpty) { + if (int.parse( + productModel!.itemAttribute!.variants! + .where((element) => element.variantSku == cartProductModel.variantInfo!.variantSku) + .first + .variantQuantity + .toString(), + ) > + (cartProductModel.quantity ?? 0) || + int.parse( + productModel!.itemAttribute!.variants! + .where((element) => element.variantSku == cartProductModel.variantInfo!.variantSku) + .first + .variantQuantity + .toString(), + ) == + -1) { + controller.addToCart(cartProductModel: cartProductModel, isIncrement: true, quantity: cartProductModel.quantity! + 1); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + } else { + if ((productModel!.quantity ?? 0) > (cartProductModel.quantity ?? 0) || productModel!.quantity == -1) { + controller.addToCart(cartProductModel: cartProductModel, isIncrement: true, quantity: cartProductModel.quantity! + 1); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + } + } else { + if ((productModel!.quantity ?? 0) > (cartProductModel.quantity ?? 0) || productModel!.quantity == -1) { + controller.addToCart(cartProductModel: cartProductModel, isIncrement: true, quantity: cartProductModel.quantity! + 1); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + } + }, + child: Icon(Icons.add, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ], + ), + ), + ), + ], + ), + cartProductModel.variantInfo == null || cartProductModel.variantInfo!.variantOptions == null || cartProductModel.variantInfo!.variantOptions!.isEmpty + ? Container() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Variants".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + const SizedBox(height: 5), + Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: + List.generate(cartProductModel.variantInfo!.variantOptions!.length, (i) { + return Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Text( + "${cartProductModel.variantInfo!.variantOptions!.keys.elementAt(i)} : ${cartProductModel.variantInfo!.variantOptions![cartProductModel.variantInfo!.variantOptions!.keys.elementAt(i)]}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey500 : AppThemeData.grey400), + ), + ), + ); + }).toList(), + ), + ], + ), + ), + cartProductModel.extras == null || cartProductModel.extras!.isEmpty || cartProductModel.extrasPrice == '0' + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: Text( + "Addons".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow( + amount: (double.parse(cartProductModel.extrasPrice.toString()) * double.parse(cartProductModel.quantity.toString())).toString(), + ), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + ], + ), + const SizedBox(height: 5), + Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: + List.generate(cartProductModel.extras!.length, (i) { + return Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Text( + cartProductModel.extras![i].toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey500 : AppThemeData.grey400), + ), + ), + ); + }).toList(), + ), + ], + ), + ], + ), + ), + ); + }, + separatorBuilder: (context, index) { + return Padding(padding: const EdgeInsets.symmetric(vertical: 10), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)); + }, + ), + ), + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${'Delivery Type'.tr} (${controller.selectedFoodType.value})".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + const SizedBox(height: 10), + controller.selectedFoodType.value == 'TakeAway' + ? const SizedBox() + : Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Instant Delivery".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + const SizedBox(height: 5), + Text( + "Standard".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + ), + Radio( + value: controller.deliveryType.value, + groupValue: "instant".tr, + activeColor: AppThemeData.primary300, + onChanged: (value) { + controller.deliveryType.value = "instant"; + }, + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: InkWell( + onTap: () { + controller.deliveryType.value = "schedule"; + BottomPicker.dateTime( + onSubmit: (index) { + controller.scheduleDateTime.value = index; + }, + minDateTime: DateTime.now(), + displaySubmitButton: true, + pickerTitle: Text('Schedule Time'.tr), + buttonSingleColor: AppThemeData.primary300, + ).show(context); + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Schedule Time".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + const SizedBox(height: 5), + Text( + "${'Your preferred time'.tr} ${controller.deliveryType.value == "schedule" ? Constant.timestampToDateTime(Timestamp.fromDate(controller.scheduleDateTime.value)) : ""}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + ), + Radio( + value: controller.deliveryType.value, + groupValue: "schedule".tr, + activeColor: AppThemeData.primary300, + onChanged: (value) { + controller.deliveryType.value = "schedule"; + BottomPicker.dateTime( + initialDateTime: controller.scheduleDateTime.value, + onSubmit: (index) { + controller.scheduleDateTime.value = index; + }, + minDateTime: controller.scheduleDateTime.value, + displaySubmitButton: true, + pickerTitle: Text('Schedule Time'.tr), + buttonSingleColor: AppThemeData.primary300, + ).show(context); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Offers & Benefits".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + Get.to(const CouponListScreen()); + }, + child: Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shadows: const [BoxShadow(color: Color(0x14000000), blurRadius: 52, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Apply Coupons".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ), + const SizedBox(height: 5), + const Icon(Icons.keyboard_arrow_right), + ], + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Bill Details".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shadows: const [BoxShadow(color: Color(0x14000000), blurRadius: 52, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Item totals".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow(amount: controller.subTotal.value.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + controller.selectedFoodType.value == 'TakeAway' + ? const SizedBox() + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Delivery Fee".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + (controller.vendorModel.value.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true) + ? Text( + 'Free Delivery'.tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: AppThemeData.success400, fontSize: 16), + ) + : Text( + Constant.amountShow(amount: controller.deliveryCharges.value.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Coupon Discount".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + "- (${Constant.amountShow(amount: controller.couponAmount.value.toString())})", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.danger300 : AppThemeData.danger300, fontSize: 16), + ), + ], + ), + controller.vendorModel.value.specialDiscountEnable == true && Constant.specialDiscountOffer == true + ? Column( + children: [ + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Special Discount".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + "- (${Constant.amountShow(amount: controller.specialDiscountAmount.value.toString())})", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.danger300 : AppThemeData.danger300, fontSize: 16), + ), + ], + ), + ], + ) + : const SizedBox(), + const SizedBox(height: 10), + controller.selectedFoodType.value == 'TakeAway' || (controller.vendorModel.value.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true) + ? const SizedBox() + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Delivery Tips".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + controller.deliveryTips.value == 0 + ? const SizedBox() + : InkWell( + onTap: () { + controller.deliveryTips.value = 0; + controller.calculatePrice(); + }, + child: Text( + "Remove".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + ), + Text( + Constant.amountShow(amount: controller.deliveryTips.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 10), + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "${taxModel.title.toString()} (${taxModel.type == "fix" ? Constant.amountShow(amount: taxModel.tax) : "${taxModel.tax}%"})", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow( + amount: + Constant.calculateTax( + amount: + (double.parse(controller.subTotal.value.toString()) - controller.couponAmount.value - controller.specialDiscountAmount.value).toString(), + taxModel: taxModel, + ).toString(), + ), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "To Pay".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow(amount: controller.totalAmount.value.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + controller.selectedFoodType.value == 'TakeAway' || (controller.vendorModel.value.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true) + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Thanks with a tip!".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shadows: const [BoxShadow(color: Color(0x14000000), blurRadius: 52, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Around the clock, our delivery partners make it happen. Show gratitude with a tip..".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600), + ), + ), + const SizedBox(width: 10), + SvgPicture.asset("assets/images/ic_tips.svg"), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + controller.deliveryTips.value = 20; + controller.calculatePrice(); + }, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: + controller.deliveryTips.value == 20 + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Text( + Constant.amountShow(amount: "20"), + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontSize: 14, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + onTap: () { + controller.deliveryTips.value = 30; + controller.calculatePrice(); + }, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: + controller.deliveryTips.value == 30 + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Text( + Constant.amountShow(amount: "30"), + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontSize: 14, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + onTap: () { + controller.deliveryTips.value = 40; + controller.calculatePrice(); + }, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: + controller.deliveryTips.value == 40 + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Text( + Constant.amountShow(amount: "40"), + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontSize: 14, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return tipsDialog(controller, isDark); + }, + ); + }, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Text( + 'Other'.tr, + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontSize: 14, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column(children: [TextFieldWidget(title: 'Remarks'.tr, controller: controller.reMarkController.value, hintText: 'Write remarks for the store'.tr, maxLine: 4)]), + ), + ], + ), + ), + bottomNavigationBar: + cartItem.isEmpty + ? null + : Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + height: controller.isCashbackApply.value == true ? 150 : 100, + child: Column( + children: [ + if (controller.isCashbackApply.value == true) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text("Cashback Offer".tr, style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontSize: 13)), + ), + Text( + "${"Cashback Name :".tr} ${controller.bestCashback.value.title ?? ''}", + style: TextStyle(color: AppThemeData.success300, fontFamily: AppThemeData.semiBold, fontSize: 13), + ), + Text( + "${"You will get".tr} ${Constant.amountShow(amount: controller.bestCashback.value.cashbackValue?.toStringAsFixed(2))} ${"cashback after completing the order.".tr}", + style: TextStyle(color: AppThemeData.success300, fontFamily: AppThemeData.semiBold, fontSize: 13), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(left: 16, right: 16, top: controller.isCashbackApply.value == false ? 16 : 12, bottom: 20), + child: Row( + children: [ + Expanded( + flex: 2, + child: InkWell( + onTap: () { + Get.to(const SelectPaymentScreen())?.then((v) { + controller.getCashback(); + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + controller.selectedPaymentMethod.value == '' + ? cardDecoration(controller, PaymentGateway.wallet, isDark, "") + : controller.selectedPaymentMethod.value == PaymentGateway.wallet.name + ? cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png") + : controller.selectedPaymentMethod.value == PaymentGateway.cod.name + ? cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png") + : controller.selectedPaymentMethod.value == PaymentGateway.stripe.name + ? cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png") + : controller.selectedPaymentMethod.value == PaymentGateway.paypal.name + ? cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payStack.name + ? cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png") + : controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name + ? cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png") + : controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name + ? cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png") + : controller.selectedPaymentMethod.value == PaymentGateway.payFast.name + ? cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png") + : controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name + ? cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png") + : controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name + ? cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png") + : controller.selectedPaymentMethod.value == PaymentGateway.xendit.name + ? cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png") + : cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + const SizedBox(width: 10), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Pay Via".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontSize: 12), + ), + controller.selectedPaymentMethod.value == '' + ? Padding(padding: const EdgeInsets.only(top: 4), child: Container(width: 60, height: 12, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100)) + : Text( + controller.selectedPaymentMethod.value, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ], + ), + ), + ), + Expanded( + child: RoundedButtonFill( + textColor: + controller.selectedPaymentMethod.value != '' + ? AppThemeData.surface + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + title: "Pay Now".tr, + height: 5, + color: + controller.selectedPaymentMethod.value != '' + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + fontSizes: 16, + onPress: () async { + if ((controller.couponAmount.value >= 1) && (controller.couponAmount.value > controller.totalAmount.value)) { + ShowToastDialog.showToast("The total price must be greater than or equal to the coupon discount value for the code to apply. Please review your cart total.".tr); + return; + } + if ((controller.specialDiscountAmount.value >= 1) && (controller.specialDiscountAmount.value > controller.totalAmount.value)) { + ShowToastDialog.showToast("The total price must be greater than or equal to the special discount value for the code to apply. Please review your cart total.".tr); + return; + } + if (controller.isOrderPlaced.value == false) { + controller.isOrderPlaced.value = true; + await controller.getCashback(); + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.totalAmount.value.toString(), context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.placeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + controller.placeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.totalAmount.value.toString()), razorpayModel: controller.razorPayModel.value).then(( + value, + ) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.totalAmount.value.toString(), orderId: result.id); + } + }); + } else { + controller.isOrderPlaced.value = false; + ShowToastDialog.showToast("Please select payment method".tr); + } + controller.isOrderPlaced.value = false; + } + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Padding cardDecoration(CartController controller, PaymentGateway value, isDark, String image) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + width: 40, + height: 40, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: image == '' ? Container(color: isDark ? AppThemeData.grey800 : AppThemeData.grey100) : Image.asset(image)), + ), + ); + } + + Dialog tipsDialog(CartController controller, isDark) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + insetPadding: const EdgeInsets.all(10), + clipBehavior: Clip.antiAliasWithSaveLayer, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + child: Padding( + padding: const EdgeInsets.all(30), + child: SizedBox( + width: 500, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + TextFieldWidget( + title: 'Tips Amount'.tr, + controller: controller.tipsController.value, + textInputType: const TextInputType.numberWithOptions(signed: true, decimal: true), + textInputAction: TextInputAction.done, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9]'))], + prefix: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Text(Constant.currencyModel!.symbol.tr, style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontSize: 18)), + ), + hintText: 'Enter Tips Amount'.tr, + ), + SizedBox(height: 10), + Row( + children: [ + Expanded( + child: RoundedButtonFill( + title: "Cancel".tr, + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + textColor: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + onPress: () async { + Get.back(); + }, + ), + ), + const SizedBox(width: 20), + Expanded( + child: RoundedButtonFill( + title: "Add".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + if (controller.tipsController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter tips Amount".tr); + } else { + controller.deliveryTips.value = double.parse(controller.tipsController.value.text); + controller.calculatePrice(); + Get.back(); + } + }, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/cart_screen/coupon_list_screen.dart b/lib/screen_ui/multi_vendor_service/cart_screen/coupon_list_screen.dart new file mode 100644 index 0000000..fec098c --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/cart_screen/coupon_list_screen.dart @@ -0,0 +1,168 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cart_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/widget/my_separator.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; + +class CouponListScreen extends StatelessWidget { + const CouponListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CartController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Coupon Code".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(55), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFieldWidget( + hintText: 'Enter coupon code'.tr, + controller: controller.couponCodeController.value, + suffix: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: InkWell( + onTap: () { + if (controller.couponCodeController.value.text.isEmpty) { + ShowToastDialog.showToast("Please enter coupon code".tr); + return; + } + CouponModel? matchedCoupon = controller.couponList.firstWhereOrNull((coupon) => coupon.code!.toLowerCase() == controller.couponCodeController.value.text.toLowerCase()); + if (matchedCoupon != null) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: matchedCoupon); + + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = matchedCoupon; + controller.calculatePrice(); + Get.back(); + } else { + ShowToastDialog.showToast("Coupon code not applied".tr); + } + } else { + ShowToastDialog.showToast("Invalid Coupon".tr); + } + }, + child: Text( + "Apply".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ), + ), + ), + ), + ), + body: ListView.builder( + shrinkWrap: true, + itemCount: controller.couponList.length, + itemBuilder: (context, index) { + CouponModel couponModel = controller.couponList[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Container( + height: Responsive.height(16, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + child: Stack( + children: [ + Image.asset("assets/images/ic_coupon_image.png", height: Responsive.height(16, context), fit: BoxFit.fill), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Align( + alignment: Alignment.center, + child: RotatedBox( + quarterTurns: -1, + child: Text( + "${couponModel.discountType == "Fix Price" ? Constant.amountShow(amount: couponModel.discount) : "${couponModel.discount}%"} ${'Off'.tr}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(6), color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "${couponModel.code}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + const Expanded(child: SizedBox(height: 10)), + InkWell( + onTap: () { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: couponModel); + + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = couponModel; + controller.calculatePrice(); + Get.back(); + } else { + ShowToastDialog.showToast("Coupon code not applied".tr); + } + }, + child: Text( + "Tap To Apply".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 20), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 20), + Text( + "${couponModel.description}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/cart_screen/oder_placing_screens.dart b/lib/screen_ui/multi_vendor_service/cart_screen/oder_placing_screens.dart new file mode 100644 index 0000000..5332234 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/cart_screen/oder_placing_screens.dart @@ -0,0 +1,228 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dash_board_controller.dart'; +import 'package:customer/controllers/dash_board_ecommarce_controller.dart'; +import 'package:customer/controllers/order_placing_controller.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/screen_ui/ecommarce/dash_board_e_commerce_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../dash_board_screens/dash_board_screen.dart'; + +class OrderPlacingScreen extends StatelessWidget { + const OrderPlacingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: OrderPlacingController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: + controller.isLoading.value + ? Constant.loader() + : controller.isPlacing.value + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Order Placed".tr, + textAlign: TextAlign.start, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey900, fontSize: 34, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w400), + ), + Text( + "Hang tight — your items are being delivered quickly and safely!".tr, + textAlign: TextAlign.start, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 40), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_location.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 10), + Expanded( + child: Text( + "Order ID".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + ), + ], + ), + const SizedBox(height: 5), + Text( + controller.orderModel.value.id.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + ], + ), + ) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Image.asset("assets/images/ic_timer.gif", height: 140)), + const SizedBox(height: 20), + Text( + "Placing your order".tr, + textAlign: TextAlign.start, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey900, fontSize: 34, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w400), + ), + Text( + "Take a moment to review your order before proceeding to checkout.".tr, + textAlign: TextAlign.start, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 40), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_location.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 10), + Expanded( + child: Text( + "Delivery Address".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + ), + ], + ), + const SizedBox(height: 5), + Text( + controller.orderModel.value.address!.getFullAddress(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_book.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn), height: 22), + const SizedBox(width: 10), + Expanded( + child: Text( + "Order Summary".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + ), + ], + ), + const SizedBox(height: 5), + ListView.builder( + shrinkWrap: true, + itemCount: controller.orderModel.value.products!.length, + itemBuilder: (context, index) { + CartProductModel cartProductModel = controller.orderModel.value.products![index]; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${cartProductModel.quantity} x".tr, + textAlign: TextAlign.start, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey900, fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + Text( + "${cartProductModel.name}".tr, + textAlign: TextAlign.start, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey900, fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ], + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: + controller.isPlacing.value + ? RoundedButtonFill( + title: "Track Order".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + if (Constant.sectionConstantModel!.serviceTypeFlag == "ecommerce-service") { + Get.offAll(const DashBoardEcommerceScreen()); + DashBoardEcommerceController controller = Get.put(DashBoardEcommerceController()); + controller.selectedIndex.value = 3; + } else { + Get.offAll(const DashBoardScreen()); + DashBoardController controller = Get.put(DashBoardController()); + controller.selectedIndex.value = 3; + } + }, + ) + : RoundedButtonFill( + title: "Track Order".tr, + height: 5.5, + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + textColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + fontSizes: 16, + onPress: () async {}, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/cart_screen/select_payment_screen.dart b/lib/screen_ui/multi_vendor_service/cart_screen/select_payment_screen.dart new file mode 100644 index 0000000..17ac46a --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/cart_screen/select_payment_screen.dart @@ -0,0 +1,284 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cart_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../wallet_screen/wallet_screen.dart'; + +class SelectPaymentScreen extends StatelessWidget { + const SelectPaymentScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CartController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + "Payment Option".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.medium, + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Preferred Payment".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox( + height: 10, + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + shadows: const [ + BoxShadow( + color: Color(0x07000000), + blurRadius: 20, + offset: Offset(0, 0), + spreadRadius: 0, + ) + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + children: [ + const SizedBox( + height: 10, + ), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox( + height: 10, + ), + ], + ), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + shadows: const [ + BoxShadow( + color: Color(0x07000000), + blurRadius: 20, + offset: Offset(0, 0), + spreadRadius: 0, + ) + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.stripeModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png"), + ), + Visibility( + visible: controller.payPalModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png"), + ), + Visibility( + visible: controller.payStackModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png"), + ), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility( + visible: controller.payFastModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png"), + ), + Visibility( + visible: controller.razorPayModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + ), + Visibility( + visible: controller.midTransModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png"), + ), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility( + visible: controller.xenditModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png"), + ), + ], + ), + ), + ) + ], + ), + ), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20))), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "${'Pay Now'.tr} | ${Constant.amountShow(amount: controller.totalAmount.value.toString())}".tr, + height: 5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + Get.back(); + }, + ), + ), + ), + ); + }, + ); + } + + Obx cardDecoration(CartController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Padding( + padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), + child: Image.asset( + image, + ), + ), + ), + const SizedBox( + width: 10, + ), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.medium, + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + Text( + Constant.amountShow(amount: controller.userModel.value.walletAmount == null ? '0.0' : controller.userModel.value.walletAmount.toString()), + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + fontSize: 16, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + ), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.medium, + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + ), + const Expanded( + child: SizedBox(), + ), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/cashback_screen/cashback_offers_list.dart b/lib/screen_ui/multi_vendor_service/cashback_screen/cashback_offers_list.dart new file mode 100644 index 0000000..33d9863 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/cashback_screen/cashback_offers_list.dart @@ -0,0 +1,78 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cashback_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; + +class CashbackOffersListScreen extends StatelessWidget { + const CashbackOffersListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CashbackController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + title: Text("Cashback Offers".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + ), + body: + controller.isLoading.value + ? Constant.loader() + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: controller.cashbackList.length, + itemBuilder: (BuildContext context, int index) { + return Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + borderRadius: BorderRadius.circular(12), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 6, offset: const Offset(0, 3))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + controller.cashbackList[index].title ?? '', + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + Text( + controller.cashbackList[index].cashbackType == 'Percent' + ? "${controller.cashbackList[index].cashbackAmount}%" + : Constant.amountShow(amount: "${controller.cashbackList[index].cashbackAmount}"), + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + const SizedBox(height: 6), + Text( + "${"Min spent".tr} ${Constant.amountShow(amount: "${controller.cashbackList[index].minimumPurchaseAmount ?? 0.0}")} | ${"Valid till".tr} ${Constant.timestampToDateTime2(controller.cashbackList[index].endDate!)}", + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontSize: 14), + ), + Text( + "${"Maximum cashback up to".tr} ${Constant.amountShow(amount: "${controller.cashbackList[index].maximumDiscount ?? 0.0}")}", + style: TextStyle(color: isDark ? AppThemeData.primary200 : AppThemeData.primary300, fontFamily: AppThemeData.regular, fontSize: 14), + ), + ], + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/change langauge/change_language_screen.dart b/lib/screen_ui/multi_vendor_service/change langauge/change_language_screen.dart new file mode 100644 index 0000000..495b9a5 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/change langauge/change_language_screen.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/change_language_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../service/localization_service.dart'; + +class ChangeLanguageScreen extends StatelessWidget { + const ChangeLanguageScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ChangeLanguageController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Change Language".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "Select your preferred language for a personalized app experience.".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 20), + Expanded( + child: GridView.count( + crossAxisCount: 2, + childAspectRatio: (1.1 / 1), + crossAxisSpacing: 5, + mainAxisSpacing: 1, + children: + controller.languageList + .map( + (data) => Obx( + () => GestureDetector( + onTap: () { + LocalizationService().changeLocale(data.slug.toString()); + Preferences.setString(Preferences.languageCodeKey, jsonEncode(data)); + controller.selectedLanguage.value = data; + }, + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + NetworkImageWidget(imageUrl: data.image.toString(), height: 80, width: 80), + // SvgPicture.network( + // data.image.toString(), + // height: 80, + // width: 80, + // fit: BoxFit.contain, + // placeholderBuilder: (context) => const Center(child: CircularProgressIndicator(strokeWidth: 1.5)), + // ), + const SizedBox(height: 5), + Text( + "${data.title}", + style: TextStyle( + fontSize: 16, + color: + controller.selectedLanguage.value.slug == data.slug + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey400 + : AppThemeData.grey500, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ), + ) + .toList(), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/chat_screens/ChatVideoContainer.dart b/lib/screen_ui/multi_vendor_service/chat_screens/ChatVideoContainer.dart new file mode 100644 index 0000000..e8a93bc --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/chat_screens/ChatVideoContainer.dart @@ -0,0 +1,9 @@ +import 'package:customer/models/conversation_model.dart'; + +class ChatVideoContainer { + Url videoUrl; + + String thumbnailUrl; + + ChatVideoContainer({required this.videoUrl, required this.thumbnailUrl}); +} diff --git a/lib/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart b/lib/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart new file mode 100644 index 0000000..d16d944 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart @@ -0,0 +1,310 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/chat_controller.dart'; +import 'package:customer/models/conversation_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import '../../../controllers/theme_controller.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../widget/firebase_pagination/src/fireStore_pagination.dart'; +import '../../../widget/firebase_pagination/src/models/view_type.dart'; +import 'ChatVideoContainer.dart'; +import 'full_screen_image_viewer.dart'; +import 'full_screen_video_viewer.dart'; + +class ChatScreen extends StatelessWidget { + const ChatScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ChatController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + controller.restaurantName.value, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + body: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: FirestorePagination( + controller: controller.scrollController, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, documentSnapshots, index) { + ConversationModel inboxModel = ConversationModel.fromJson(documentSnapshots[index].data() as Map); + return chatItemView(isDark, inboxModel.senderId == FireStoreUtils.getCurrentUid(), inboxModel); + }, + onEmpty: Constant.showEmptyView(message: "No Conversion found".tr), + // orderBy is compulsory to enable pagination + query: FirebaseFirestore.instance + .collection( + controller.chatType.value == "Driver" + ? 'chat_driver' + : controller.chatType.value == "Provider" || controller.chatType.value == "provider" + ? 'chat_provider' + : controller.chatType.value == "worker" || controller.chatType.value == "Worker" + ? 'chat_worker' + : 'chat_store', + ) + .doc(controller.orderId.value) + .collection("thread") + .orderBy('createdAt', descending: false), + isLive: true, + viewType: ViewType.list, + ), + ), + ), + Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () { + onCameraClick(context, controller); + }, + child: SvgPicture.asset("assets/icons/ic_picture_one.svg"), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: TextField( + textInputAction: TextInputAction.send, + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.sentences, + controller: controller.messageController.value, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(top: 3, left: 10), + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + hintText: 'Type message here....'.tr, + ), + onSubmitted: (value) async { + if (controller.messageController.value.text.isNotEmpty) { + controller.sendMessage(controller.messageController.value.text, null, '', 'text'); + Timer(const Duration(milliseconds: 500), () => controller.scrollController.jumpTo(controller.scrollController.position.maxScrollExtent)); + controller.messageController.value.clear(); + } + }, + ), + ), + ), + InkWell( + onTap: () { + if (controller.messageController.value.text.isNotEmpty) { + controller.sendMessage(controller.messageController.value.text, null, '', 'text'); + controller.messageController.value.clear(); + // Timer(const Duration(milliseconds: 500), () => controller.scrollController.jumpTo(controller.scrollController.position.maxScrollExtent)); + } + }, + child: Container( + margin: const EdgeInsets.only(left: 10), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, borderRadius: BorderRadius.circular(30)), + child: Padding(padding: const EdgeInsets.all(10), child: SvgPicture.asset("assets/icons/ic_send.svg")), + ), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget chatItemView(isDark, bool isMe, ConversationModel data) { + return Container( + padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + child: + isMe + ? Align( + alignment: Alignment.topRight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + data.messageType == "text" + ? Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12), bottomLeft: Radius.circular(12)), + color: AppThemeData.primary300, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text(data.message.toString(), style: const TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: AppThemeData.grey50)), + ) + : data.messageType == "image" + ? ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12), bottomLeft: Radius.circular(12)), + child: Stack( + alignment: Alignment.center, + children: [ + GestureDetector( + onTap: () { + Get.to(FullScreenImageViewer(imageUrl: data.url!.url)); + }, + child: Hero(tag: data.url!.url, child: NetworkImageWidget(imageUrl: data.url!.url, height: 100, width: 100, fit: BoxFit.cover)), + ), + ], + ), + ) + : FloatingActionButton( + mini: true, + heroTag: data.id, + backgroundColor: AppThemeData.primary300, + onPressed: () { + Get.to(FullScreenVideoViewer(heroTag: data.id.toString(), videoUrl: data.url!.url)); + }, + child: const Icon(Icons.play_arrow, color: Colors.white), + ), + const SizedBox(height: 5), + Text( + DateFormat('MMM d, yyyy hh:mm aa').format(DateTime.fromMillisecondsSinceEpoch(data.createdAt!.millisecondsSinceEpoch)), + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + data.messageType == "text" + ? Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12), bottomRight: Radius.circular(12)), + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text(data.message.toString(), style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800)), + ) + : data.messageType == "image" + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 50, maxWidth: 200), + child: ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12), bottomRight: Radius.circular(12)), + child: Stack( + alignment: Alignment.center, + children: [ + GestureDetector( + onTap: () { + Get.to(FullScreenImageViewer(imageUrl: data.url!.url)); + }, + child: Hero(tag: data.url!.url, child: NetworkImageWidget(imageUrl: data.url!.url)), + ), + ], + ), + ), + ) + : FloatingActionButton( + mini: true, + heroTag: data.id, + backgroundColor: AppThemeData.primary300, + onPressed: () { + Get.to(FullScreenVideoViewer(heroTag: data.id.toString(), videoUrl: data.url!.url)); + }, + child: const Icon(Icons.play_arrow, color: Colors.white), + ), + const SizedBox(height: 5), + Text( + DateFormat('MMM d, yyyy hh:mm aa').format(DateTime.fromMillisecondsSinceEpoch(data.createdAt!.millisecondsSinceEpoch)), + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + ); + } + + void onCameraClick(BuildContext context, ChatController controller) { + final action = CupertinoActionSheet( + message: Text('Send Media'.tr, style: const TextStyle(fontSize: 15.0)), + actions: [ + CupertinoActionSheetAction( + isDefaultAction: false, + onPressed: () async { + Get.back(); + XFile? image = await controller.imagePicker.pickImage(source: ImageSource.gallery); + if (image != null) { + Url url = await FireStoreUtils.uploadChatImageToFireStorage(File(image.path), context); + controller.sendMessage('', url, '', 'image'); + } + }, + child: Text("Choose image from gallery".tr), + ), + CupertinoActionSheetAction( + isDefaultAction: false, + onPressed: () async { + Get.back(); + XFile? galleryVideo = await controller.imagePicker.pickVideo(source: ImageSource.gallery); + if (galleryVideo != null) { + ChatVideoContainer? videoContainer = await FireStoreUtils.uploadChatVideoToFireStorage(context, File(galleryVideo.path)); + if (videoContainer != null) { + controller.sendMessage('', videoContainer.videoUrl, videoContainer.thumbnailUrl, 'video'); + } + } + }, + child: Text("Choose video from gallery".tr), + ), + CupertinoActionSheetAction( + isDestructiveAction: false, + onPressed: () async { + Get.back(); + XFile? image = await controller.imagePicker.pickImage(source: ImageSource.camera); + if (image != null) { + Url url = await FireStoreUtils.uploadChatImageToFireStorage(File(image.path), context); + controller.sendMessage('', url, '', 'image'); + } + }, + child: Text("Take a picture".tr), + ), + // CupertinoActionSheetAction( + // isDestructiveAction: false, + // onPressed: () async { + // Get.back(); + // XFile? recordedVideo = await controller.imagePicker.pickVideo(source: ImageSource.camera); + // if (recordedVideo != null) { + // ChatVideoContainer videoContainer = await FireStoreUtils.uploadChatVideoToFireStorage(File(recordedVideo.path), context); + // controller.sendMessage('', videoContainer.videoUrl, videoContainer.thumbnailUrl, 'video'); + // } + // }, + // child: Text("Record video".tr), + // ) + ], + cancelButton: CupertinoActionSheetAction( + child: Text('Cancel'.tr), + onPressed: () { + Get.back(); + }, + ), + ); + showCupertinoModalPopup(context: context, builder: (context) => action); + } +} diff --git a/lib/screen_ui/multi_vendor_service/chat_screens/driver_inbox_screen.dart b/lib/screen_ui/multi_vendor_service/chat_screens/driver_inbox_screen.dart new file mode 100644 index 0000000..9572a40 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/chat_screens/driver_inbox_screen.dart @@ -0,0 +1,127 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/inbox_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/firebase_pagination/src/fireStore_pagination.dart'; +import '../../../widget/firebase_pagination/src/models/view_type.dart'; +import 'chat_screen.dart'; + +class DriverInboxScreen extends StatelessWidget { + const DriverInboxScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Driver Inbox".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: FirestorePagination( + //item builder type is compulsory. + physics: const BouncingScrollPhysics(), + itemBuilder: (context, documentSnapshots, index) { + final data = documentSnapshots[index].data() as Map?; + InboxModel inboxModel = InboxModel.fromJson(data!); + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(inboxModel.customerId.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(inboxModel.restaurantId.toString()); + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": restaurantUser!.fullName(), + "orderId": inboxModel.orderId, + "restaurantId": restaurantUser.id, + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": restaurantUser.profilePictureURL, + "token": restaurantUser.fcmToken, + "chatType": inboxModel.chatType, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: NetworkImageWidget( + imageUrl: inboxModel.restaurantProfileImage.toString(), + fit: BoxFit.cover, + height: Responsive.height(6, context), + width: Responsive.width(12, context), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "${inboxModel.restaurantName}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ), + Text( + Constant.timestampToDate(inboxModel.createdAt!), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + const SizedBox(height: 5), + Text( + "${inboxModel.lastMessage}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + shrinkWrap: true, + onEmpty: Constant.showEmptyView(message: "No Conversion found".tr), + // orderBy is compulsory to enable pagination + query: FirebaseFirestore.instance.collection('chat_driver').where("customerId", isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true), + //Change types customerId + viewType: ViewType.list, + initialLoader: Constant.loader(), + // to fetch real-time data + isLive: true, + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/chat_screens/full_screen_image_viewer.dart b/lib/screen_ui/multi_vendor_service/chat_screens/full_screen_image_viewer.dart new file mode 100644 index 0000000..29e01fc --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/chat_screens/full_screen_image_viewer.dart @@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:photo_view/photo_view.dart'; + +class FullScreenImageViewer extends StatelessWidget { + final String imageUrl; + final File? imageFile; + + const FullScreenImageViewer({super.key, required this.imageUrl, this.imageFile}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0.0, + backgroundColor: Colors.black, + iconTheme: const IconThemeData(color: Colors.white), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + body: Container( + color: Colors.black, + child: Hero( + tag: imageUrl, + child: PhotoView( + imageProvider: imageFile == null ? NetworkImage(imageUrl) : Image.file(imageFile!).image, + ), + ), + )); + } +} diff --git a/lib/screen_ui/multi_vendor_service/chat_screens/full_screen_video_viewer.dart b/lib/screen_ui/multi_vendor_service/chat_screens/full_screen_video_viewer.dart new file mode 100644 index 0000000..1cd857c --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/chat_screens/full_screen_video_viewer.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_player/video_player.dart'; + +class FullScreenVideoViewer extends StatefulWidget { + final String videoUrl; + final String heroTag; + final File? videoFile; + + const FullScreenVideoViewer({super.key, required this.videoUrl, required this.heroTag, this.videoFile}); + + @override + _FullScreenVideoViewerState createState() => _FullScreenVideoViewerState(); +} + +class _FullScreenVideoViewerState extends State { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.videoFile == null ? VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) : VideoPlayerController.file(widget.videoFile!) + ..initialize().then((_) { + // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. + setState(() {}); + }); + _controller.setLooping(true); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0.0, + backgroundColor: Colors.black, + iconTheme: const IconThemeData(color: Colors.white), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + body: Container( + color: Colors.black, + child: Hero( + tag: widget.videoUrl, + child: Center( + child: _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : Container(), + ), + )), + floatingActionButton: FloatingActionButton( + heroTag: widget.heroTag, + onPressed: () { + setState(() { + _controller.value.isPlaying ? _controller.pause() : _controller.play(); + }); + }, + child: Icon( + _controller.value.isPlaying ? CupertinoIcons.pause : CupertinoIcons.play_arrow_solid, + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } +} diff --git a/lib/screen_ui/multi_vendor_service/chat_screens/restaurant_inbox_screen.dart b/lib/screen_ui/multi_vendor_service/chat_screens/restaurant_inbox_screen.dart new file mode 100644 index 0000000..f7d6948 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/chat_screens/restaurant_inbox_screen.dart @@ -0,0 +1,129 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/inbox_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/widget/firebase_pagination/src/fireStore_pagination.dart'; +import 'package:customer/widget/firebase_pagination/src/models/view_type.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import 'chat_screen.dart'; + +class RestaurantInboxScreen extends StatelessWidget { + const RestaurantInboxScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Store Inbox".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: FirestorePagination( + //item builder type is compulsory. + physics: const BouncingScrollPhysics(), + itemBuilder: (context, documentSnapshots, index) { + final data = documentSnapshots[index].data() as Map?; + InboxModel inboxModel = InboxModel.fromJson(data!); + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(inboxModel.customerId.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(inboxModel.restaurantId.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(restaurantUser!.vendorID.toString()); + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": vendorModel!.title, + "orderId": inboxModel.orderId, + "restaurantId": restaurantUser.id, + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": vendorModel.photo, + "token": restaurantUser.fcmToken, + "chatType": inboxModel.chatType, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: NetworkImageWidget( + imageUrl: inboxModel.restaurantProfileImage.toString(), + fit: BoxFit.cover, + height: Responsive.height(6, context), + width: Responsive.width(12, context), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "${inboxModel.restaurantName}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ), + Text( + Constant.timestampToDate(inboxModel.createdAt!), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + const SizedBox(height: 5), + Text( + "${inboxModel.lastMessage}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + shrinkWrap: true, + onEmpty: Constant.showEmptyView(message: "No Conversion found".tr), + // orderBy is compulsory to enable pagination + query: FirebaseFirestore.instance.collection('chat_store').where("customerId", isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true), + //Change types customerId + viewType: ViewType.list, + initialLoader: Constant.loader(), + // to fetch real-time data + isLive: true, + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dash_board_screens/dash_board_screen.dart b/lib/screen_ui/multi_vendor_service/dash_board_screens/dash_board_screen.dart new file mode 100644 index 0000000..485f304 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dash_board_screens/dash_board_screen.dart @@ -0,0 +1,83 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dash_board_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; + +class DashBoardScreen extends StatelessWidget { + const DashBoardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: DashBoardController(), + builder: (controller) { + return Scaffold( + body: controller.pageList[controller.selectedIndex.value], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + showUnselectedLabels: true, + showSelectedLabels: true, + selectedFontSize: 12, + selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + currentIndex: controller.selectedIndex.value, + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + selectedItemColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + unselectedItemColor: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + onTap: (int index) { + if (index == 0) { + Get.put(DashBoardController()); + } + controller.selectedIndex.value = index; + }, + items: + Constant.walletSetting == false + ? [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_orders.svg", label: 'Orders'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ] + : [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_wallet.svg", label: 'Wallet'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_orders.svg", label: 'Orders'.tr, controller: controller), + navigationBarItem(isDark, index: 4, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ], + ), + ); + }, + ); + }); + } + + BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required DashBoardController controller}) { + return BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: SvgPicture.asset( + assetIcon, + height: 22, + width: 22, + color: + controller.selectedIndex.value == index + ? isDark + ? AppThemeData.primary300 + : AppThemeData.primary300 + : isDark + ? AppThemeData.grey300 + : AppThemeData.grey600, + ), + ), + label: label, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_details.dart b/lib/screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_details.dart new file mode 100644 index 0000000..b08e3e9 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_details.dart @@ -0,0 +1,272 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dine_in_booking_details_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../controllers/theme_controller.dart'; + +class DineInBookingDetails extends StatelessWidget { + const DineInBookingDetails({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: DineInBookingDetailsController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + title: Text( + "Dine in Bookings".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${'Order'.tr} ${Constant.orderId(orderId: controller.bookingModel.value.id.toString())}", + style: TextStyle(fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + Text( + "${controller.bookingModel.value.totalGuest} ${'Peoples'.tr}", + style: TextStyle(fontSize: 14, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ], + ), + ), + Container( + decoration: ShapeDecoration( + color: Constant.statusColor(status: controller.bookingModel.value.status), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Text( + "${controller.bookingModel.value.status}", + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ), + ), + ], + ), + const SizedBox(height: 20), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_building.svg"), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.bookingModel.value.vendor!.title.toString(), + style: TextStyle(fontSize: 18, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + Text( + controller.bookingModel.value.vendor!.location.toString(), + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () { + launchUrl( + Constant.createCoordinatesUrl( + controller.bookingModel.value.vendor!.latitude ?? 0.0, + controller.bookingModel.value.vendor!.longitude ?? 0.0, + controller.bookingModel.value.vendor!.title, + ), + ); + }, + child: Text( + "View in Map".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + decorationColor: AppThemeData.primary300, + ), + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: SizedBox(height: 16, child: VerticalDivider(width: 1))), + InkWell( + onTap: () { + if (controller.bookingModel.value.vendor!.phonenumber!.isNotEmpty) { + final Uri launchUri = Uri(scheme: 'tel', path: controller.bookingModel.value.vendor!.phonenumber); + launchUrl(launchUri); + } + }, + child: Text( + "Call Now".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + decorationColor: AppThemeData.primary300, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + const SizedBox(height: 20), + Text( + "Booking Details".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + const SizedBox(height: 5), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Name".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ), + Expanded( + child: Text( + "${controller.bookingModel.value.guestFirstName} ${controller.bookingModel.value.guestLastName}", + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Phone number".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ), + Expanded( + child: Text( + "${controller.bookingModel.value.guestPhone}", + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Date and Time".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ), + Expanded( + child: Text( + Constant.timestampToDateTime(controller.bookingModel.value.date!), + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Guest".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ), + Expanded( + child: Text( + "${controller.bookingModel.value.totalGuest}", + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Discount".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ), + Expanded( + child: Text( + "${controller.bookingModel.value.discount} %", + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_screen.dart b/lib/screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_screen.dart new file mode 100644 index 0000000..af8a5a3 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_booking/dine_in_booking_screen.dart @@ -0,0 +1,259 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dine_in_booking_controller.dart'; +import 'package:customer/models/dine_in_booking_model.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../themes/app_them_data.dart'; +import '../../../widget/my_separator.dart'; +import 'dine_in_booking_details.dart'; + +class DineInBookingScreen extends StatelessWidget { + const DineInBookingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: DineInBookingController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + title: Text( + "Dine in Bookings".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + controller.isFeature.value = true; + }, + child: Container( + decoration: + controller.isFeature.value == false + ? null + : ShapeDecoration(color: AppThemeData.primary300, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "Upcoming".tr, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + color: + controller.isFeature.value == false + ? isDark + ? AppThemeData.grey400 + : AppThemeData.grey500 + : isDark + ? AppThemeData.grey50 + : AppThemeData.grey50, + ), + ), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: () { + controller.isFeature.value = false; + }, + child: Container( + decoration: + controller.isFeature.value == true + ? null + : ShapeDecoration(color: AppThemeData.primary300, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "History".tr, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + color: + controller.isFeature.value == true + ? isDark + ? AppThemeData.grey400 + : AppThemeData.grey500 + : isDark + ? AppThemeData.grey50 + : AppThemeData.grey50, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 10), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: + controller.isFeature.value + ? controller.featureList.isEmpty + ? Constant.showEmptyView(message: "Upcoming Booking not found.".tr) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + scrollDirection: Axis.vertical, + itemCount: controller.featureList.length, + itemBuilder: (BuildContext context, int index) { + DineInBookingModel dineBookingModel = controller.featureList[index]; + return itemView(isDark, context, dineBookingModel); + }, + ) + : controller.historyList.isEmpty + ? Constant.showEmptyView(message: "History not found.".tr) + : ListView.builder( + itemCount: controller.historyList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + DineInBookingModel dineBookingModel = controller.historyList[index]; + return itemView(isDark, context, dineBookingModel); + }, + ), + ), + ), + ], + ), + ); + }, + ); + } + + InkWell itemView(isDark, BuildContext context, DineInBookingModel orderModel) { + return InkWell( + onTap: () { + Get.to(const DineInBookingDetails(), arguments: {"bookingModel": orderModel}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: orderModel.vendor!.photo.toString(), fit: BoxFit.cover, height: Responsive.height(10, context), width: Responsive.width(20, context)), + Container( + height: Responsive.height(10, context), + width: Responsive.width(20, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, 1.00), end: const Alignment(0, -1), colors: [Colors.black.withOpacity(0), AppThemeData.grey900]), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + orderModel.status.toString(), + textAlign: TextAlign.right, + style: TextStyle(color: Constant.statusColor(status: orderModel.status.toString()), fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500, fontSize: 12), + ), + const SizedBox(height: 5), + Text( + orderModel.vendor!.title.toString(), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 5), + Text( + Constant.timestampToDateTime(orderModel.createdAt!), + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + Padding(padding: const EdgeInsets.symmetric(vertical: 14), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: Text("Name".tr, style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400))), + Expanded( + child: Text( + "${orderModel.guestFirstName} ${orderModel.guestLastName}", + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 5), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text("Guest Number".tr, style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400)), + ), + Expanded( + child: Text( + orderModel.totalGuest.toString(), + textAlign: TextAlign.end, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ), + ], + ), + Padding(padding: const EdgeInsets.symmetric(vertical: 14), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_location.svg"), + const SizedBox(width: 10), + Expanded( + child: Text( + orderModel.vendor!.location.toString(), + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_screeen/book_table_screen.dart b/lib/screen_ui/multi_vendor_service/dine_in_screeen/book_table_screen.dart new file mode 100644 index 0000000..2a5105f --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_screeen/book_table_screen.dart @@ -0,0 +1,390 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dine_in_restaurant_details_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +import '../../../controllers/theme_controller.dart'; + +class BookTableScreen extends StatelessWidget { + const BookTableScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: DineInRestaurantDetailsController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + title: Text("Book Table".tr, style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500)), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Expanded( + child: Text( + "Numbers of Guests".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ), + Container( + height: Responsive.height(4, context), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(200), side: BorderSide(color: isDark ? AppThemeData.grey600 : AppThemeData.grey300)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () { + if (controller.noOfQuantity.value != 1) { + controller.noOfQuantity.value -= 1; + } + }, + child: const Icon(Icons.remove), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + controller.noOfQuantity.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + InkWell( + onTap: () { + controller.noOfQuantity.value += 1; + }, + child: Icon(Icons.add, color: AppThemeData.primary300), + ), + ], + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "When are you visiting?".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + SizedBox( + height: 120, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), + physics: const BouncingScrollPhysics(), + itemCount: controller.dateList.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 8), + child: GestureDetector( + onTap: () { + controller.selectedDate.value = controller.dateList[index].date; + controller.timeSet(controller.dateList[index].date); + }, + child: Obx( + () => Container( + width: 100, + height: 90, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: + controller.selectedDate.value == controller.dateList[index].date + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + Constant.calculateDifference(controller.dateList[index].date.toDate()) == 0 + ? "Today".tr + : Constant.calculateDifference(controller.dateList[index].date.toDate()) == 1 + ? "Tomorrow".tr + : DateFormat('EEE').format(controller.dateList[index].date.toDate()), + style: TextStyle( + fontSize: 12, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w500, + ), + ), + Text( + DateFormat('d MMM').format(controller.dateList[index].date.toDate()).toString(), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ), + ), + Positioned( + bottom: 10, + left: 0, + right: 0, + child: Center( + child: RoundedButtonFill( + title: "${controller.dateList[index].discountPer}%".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + width: 12, + height: 3, + onPress: () {}, + ), + ), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 10), + Text( + "Select time slot and scroll to see offers".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: isDark ? AppThemeData.grey600 : AppThemeData.grey300), borderRadius: BorderRadius.circular(12)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + child: Wrap( + spacing: 5.0, + children: [ + ...controller.timeSlotList.map( + (timeSlotList) => InputChip( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + side: BorderSide.none, + backgroundColor: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + selectedColor: AppThemeData.primary300, + labelStyle: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + label: Text( + DateFormat('hh:mm a').format(timeSlotList.time!), + style: TextStyle( + color: + controller.selectedTimeSlot.value == DateFormat('hh:mm a').format(timeSlotList.time!) + ? AppThemeData.grey50 + : isDark + ? AppThemeData.grey400 + : AppThemeData.grey500, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + ), + ), + showCheckmark: false, + selected: controller.selectedTimeSlot.value == DateFormat('hh:mm a').format(timeSlotList.time!), + onSelected: (value) { + controller.selectedTimeSlot.value = DateFormat('hh:mm a').format(timeSlotList.time!); + controller.selectedTimeDiscount.value = timeSlotList.discountPer!; + controller.selectedTimeDiscountType.value = timeSlotList.discountType!; + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: Text( + "Special Occasion".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + ), + InkWell( + onTap: () { + controller.selectedOccasion.value = ""; + }, + child: Text("Clear".tr, style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500)), + ), + ], + ), + const SizedBox(height: 10), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < controller.occasionList[i].length; i++) + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 0.0), + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + dense: true, + title: Text( + //'${controller.occasionList[i]}'.tr, + controller.getLocalizedOccasion(controller.occasionList[i]), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + leading: Radio( + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + value: controller.occasionList[i], + groupValue: controller.selectedOccasion.value, + activeColor: AppThemeData.primary300, + onChanged: (value) { + controller.selectedOccasion.value = controller.occasionList[i]; + }, + ), + ), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 0.0), + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + dense: true, + title: Text( + 'Is this your first visit?'.tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + leading: Checkbox( + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + value: controller.firstVisit.value, + activeColor: AppThemeData.primary300, + onChanged: (value) { + controller.firstVisit.value = value!; + }, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Text( + "Personal Details".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipOval( + child: NetworkImageWidget( + imageUrl: Constant.userModel!.profilePictureURL.toString(), + width: 50, + height: 50, + errorWidget: Image.asset(Constant.userPlaceHolder, fit: BoxFit.cover, width: 50, height: 50), + ), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Constant.userModel!.fullName(), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "${Constant.userModel!.email}", + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Text( + "Additional Requests".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + TextFieldWidget(controller: controller.additionRequestController.value, hintText: 'Add message here....'.tr, maxLine: 5), + const SizedBox(height: 20), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Book Now".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + controller.orderBook(); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_details_screen.dart b/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_details_screen.dart new file mode 100644 index 0000000..ca857d2 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_details_screen.dart @@ -0,0 +1,768 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dine_in_restaurant_details_controller.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../chat_screens/full_screen_image_viewer.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../review_list_screen/review_list_screen.dart'; +import 'book_table_screen.dart'; + +class DineInDetailsScreen extends StatelessWidget { + const DineInDetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: DineInRestaurantDetailsController(), + builder: (controller) { + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + expandedHeight: Responsive.height(30, context), + floating: true, + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Row( + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.arrow_back, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + const Expanded(child: SizedBox()), + InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == controller.vendorModel.value.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: controller.vendorModel.value.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == controller.vendorModel.value.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: controller.vendorModel.value.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == controller.vendorModel.value.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg", colorFilter: const ColorFilter.mode(AppThemeData.grey50, BlendMode.srcIn)) + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ], + ), + flexibleSpace: FlexibleSpaceBar( + background: Stack( + children: [ + controller.vendorModel.value.photos == null || controller.vendorModel.value.photos!.isEmpty + ? Stack( + children: [ + NetworkImageWidget( + imageUrl: controller.vendorModel.value.photo.toString(), + fit: BoxFit.cover, + width: Responsive.width(100, context), + height: Responsive.height(40, context), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), Colors.black]), + ), + ), + ], + ) + : PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.vendorModel.value.photos!.length, + padEnds: false, + pageSnapping: true, + onPageChanged: (value) { + controller.currentPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + String image = controller.vendorModel.value.photos![index]; + return Stack( + children: [ + NetworkImageWidget(imageUrl: image.toString(), fit: BoxFit.cover, width: Responsive.width(100, context), height: Responsive.height(40, context)), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), Colors.black]), + ), + ), + ], + ); + }, + ), + Positioned( + bottom: 10, + right: 0, + left: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.vendorModel.value.photos!.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentPage.value == index ? AppThemeData.primary300 : AppThemeData.grey300), + ), + ); + }), + ), + ), + ], + ), + ), + ), + ]; + }, + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.vendorModel.value.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 22, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + SizedBox( + width: Responsive.width(78, context), + child: Text( + controller.vendorModel.value.location.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ), + ], + ), + ), + Column( + children: [ + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + Constant.calculateReview( + reviewCount: controller.vendorModel.value.reviewsCount!.toStringAsFixed(0), + reviewSum: controller.vendorModel.value.reviewsSum.toString(), + ), + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + const SizedBox(height: 5), + InkWell( + onTap: () { + Get.to(const ReviewListScreen(), arguments: {"vendorModel": controller.vendorModel.value}); + }, + child: Text( + "${controller.vendorModel.value.reviewsCount} ${'Ratings'.tr}", + style: TextStyle(decoration: TextDecoration.underline, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700, fontFamily: AppThemeData.regular), + ), + ), + ], + ), + ], + ), + Row( + children: [ + Text( + controller.isOpen.value ? "Open".tr : "Close".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: controller.isOpen.value ? AppThemeData.success400 : AppThemeData.danger300, + ), + ), + Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Icon(Icons.circle, size: 5, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500)), + InkWell( + onTap: () { + timeShowBottomSheet(context, controller); + }, + child: Text( + "View Timings".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.underline, + decorationColor: AppThemeData.ecommerce300, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + ), + ), + ), + Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Icon(Icons.circle, size: 5, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500)), + Text( + "${Constant.amountShow(amount: controller.vendorModel.value.restaurantCost)} ${'for two'.tr}".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Also applicable on food delivery".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + if (Constant.userModel == null) { + ShowToastDialog.showToast("Please log in to the application. You are not logged in.".tr); + } else { + Get.to(const BookTableScreen(), arguments: {"vendorModel": controller.vendorModel.value}); + } + }, + child: Container( + height: 80, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + borderRadius: BorderRadius.circular(16), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Image.asset("assets/images/ic_table.gif"), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Table Booking".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + Text( + "Quick Conformations".tr, + style: TextStyle( + fontSize: 12, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right), + ], + ), + ), + ), + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": controller.vendorModel.value}); + }, + child: Container( + height: 80, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + borderRadius: BorderRadius.circular(16), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding(padding: const EdgeInsets.all(4), child: Image.asset("assets/images/food_delivery.gif")), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Available food delivery".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + Text( + "in 30-45 mins.".tr, + style: TextStyle( + fontSize: 12, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right), + ], + ), + ), + ), + ), + ], + ), + controller.vendorModel.value.restaurantMenuPhotos == null || controller.vendorModel.value.restaurantMenuPhotos!.isEmpty + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Menu".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + SizedBox( + height: Responsive.height(12, context), + child: ListView.builder( + itemCount: controller.vendorModel.value.restaurantMenuPhotos!.length, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + return InkWell( + onTap: () { + Get.to(FullScreenImageViewer(imageUrl: controller.vendorModel.value.restaurantMenuPhotos![index])); + }, + child: Padding( + padding: const EdgeInsets.all(6.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget( + imageUrl: controller.vendorModel.value.restaurantMenuPhotos![index], + height: Responsive.height(12, context), + width: Responsive.height(12, context), + fit: BoxFit.fill, + ), + ), + ), + ); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Location, Timing & Costs".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_location.svg"), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.vendorModel.value.location.toString(), + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + ), + ), + InkWell( + onTap: () { + launchUrl( + Constant.createCoordinatesUrl( + controller.vendorModel.value.latitude ?? 0.0, + controller.vendorModel.value.longitude ?? 0.0, + controller.vendorModel.value.title, + ), + ); + }, + child: Text( + "View on Map".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_alarm_clock.svg", height: 20), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Timing".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + ), + ), + InkWell( + onTap: () {}, + child: Text( + "${controller.vendorModel.value.openDineTime == '' ? "10:00 AM" : controller.vendorModel.value.openDineTime.toString()} ${"To".tr} ${controller.vendorModel.value.closeDineTime == '' ? "10:00 PM" : controller.vendorModel.value.closeDineTime.toString()}", + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Constant.currencyModel!.symbol.toString(), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 24, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w400, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + const SizedBox(width: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Cost for Two".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + ), + ), + Text( + "${Constant.amountShow(amount: controller.vendorModel.value.restaurantCost ?? "0.0")} ${'(approx)'.tr}", + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 16, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + ), + ), + ], + ), + ], + ), + ], + ), + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Cuisines".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + Wrap( + spacing: 5.0, + children: [ + ...controller.tags.map( + (tag) => FilterChip( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + side: BorderSide.none, + backgroundColor: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + labelStyle: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + label: Text("$tag"), + onSelected: (bool value) {}, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Future timeShowBottomSheet(BuildContext context, DineInRestaurantDetailsController productModel) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(30))), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: + (context) => FractionallySizedBox( + heightFactor: 0.70, + child: StatefulBuilder( + builder: (context1, setState) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Container( + width: 134, + height: 5, + margin: const EdgeInsets.only(bottom: 6), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey50 : AppThemeData.grey800, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3))), + ), + ), + ), + Expanded( + child: ListView.builder( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemCount: productModel.vendorModel.value.workingHours!.length, + itemBuilder: (context, dayIndex) { + WorkingHours workingHours = productModel.vendorModel.value.workingHours![dayIndex]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${workingHours.day}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + workingHours.timeslot == null || workingHours.timeslot!.isEmpty + ? const SizedBox() + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: workingHours.timeslot!.length, + itemBuilder: (context, timeIndex) { + Timeslot timeSlotModel = workingHours.timeslot![timeIndex]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: isDark ? AppThemeData.grey400 : AppThemeData.grey200), + ), + child: Center( + child: Text( + timeSlotModel.from.toString(), + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: isDark ? AppThemeData.grey400 : AppThemeData.grey200), + ), + child: Center( + child: Text( + timeSlotModel.to.toString(), + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_restaurant_list_screen.dart b/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_restaurant_list_screen.dart new file mode 100644 index 0000000..e80c5a8 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_restaurant_list_screen.dart @@ -0,0 +1,356 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/restaurant_list_controller.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../widget/restaurant_image_view.dart'; +import 'dine_in_details_screen.dart'; + +class DineInRestaurantListScreen extends StatelessWidget { + const DineInRestaurantListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RestaurantListController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: + isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + controller.title.value, + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.medium, + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.vendorSearchList.length, + itemBuilder: (context, index) { + VendorModel vendorModel = + controller.vendorSearchList[index]; + return InkWell( + onTap: () { + Get.to( + const DineInDetailsScreen(), + arguments: {"vendorModel": vendorModel}, + ); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration( + color: + isDark + ? AppThemeData.grey900 + : AppThemeData.grey50, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + child: Stack( + children: [ + RestaurantImageView( + vendorModel: vendorModel, + ), + Container( + height: Responsive.height( + 20, + context, + ), + width: Responsive.width( + 100, + context, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment( + -0.00, + -1.00, + ), + end: const Alignment(0, 1), + colors: [ + Colors.black.withOpacity(0), + const Color(0xFF111827), + ], + ), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList + .where( + (p0) => + p0.restaurantId == + vendorModel.id, + ) + .isNotEmpty) { + FavouriteModel + favouriteModel = + FavouriteModel( + restaurantId: + vendorModel.id, + userId: + FireStoreUtils + .getCurrentUid(), + ); + controller.favouriteList + .removeWhere( + (item) => + item.restaurantId == + vendorModel.id, + ); + await FireStoreUtils + .removeFavouriteRestaurant( + favouriteModel, + ); + } else { + FavouriteModel + favouriteModel = + FavouriteModel( + restaurantId: + vendorModel.id, + userId: + FireStoreUtils + .getCurrentUid(), + ); + controller.favouriteList + .add(favouriteModel); + await FireStoreUtils + .setFavouriteRestaurant( + favouriteModel, + ); + } + }, + child: Obx( + () => + controller.favouriteList + .where( + (p0) => + p0.restaurantId == + vendorModel + .id, + ) + .isNotEmpty + ? SvgPicture.asset( + "assets/icons/ic_like_fill.svg", + ) + : SvgPicture.asset( + "assets/icons/ic_like.svg", + ), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset( + Responsive.width(-3, context), + Responsive.height(17.5, context), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Container( + decoration: ShapeDecoration( + color: + isDark + ? AppThemeData + .primary600 + : AppThemeData + .primary50, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 120, + ), + ), + ), + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + SvgPicture.asset( + "assets/icons/ic_star.svg", + colorFilter: + ColorFilter.mode( + AppThemeData + .primary300, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + color: + isDark + ? AppThemeData + .primary300 + : AppThemeData + .primary300, + fontFamily: + AppThemeData + .semiBold, + fontWeight: + FontWeight.w600, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Container( + decoration: ShapeDecoration( + color: + isDark + ? AppThemeData + .ecommerce600 + : AppThemeData + .ecommerce50, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 120, + ), + ), + ), + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + SvgPicture.asset( + "assets/icons/ic_map_distance.svg", + colorFilter: + ColorFilter.mode( + AppThemeData + .ecommerce300, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + color: + isDark + ? AppThemeData + .ecommerce300 + : AppThemeData + .ecommerce300, + fontFamily: + AppThemeData + .semiBold, + fontWeight: + FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: + isDark + ? AppThemeData.grey50 + : AppThemeData.grey900, + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: + isDark + ? AppThemeData.grey400 + : AppThemeData.grey400, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_screen.dart b/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_screen.dart new file mode 100644 index 0000000..9393aa7 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_screeen/dine_in_screen.dart @@ -0,0 +1,830 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dine_in_controller.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/dine_in_screeen/view_all_category_dine_in_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../models/banner_model.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/restaurant_image_view.dart'; +import '../home_screen/category_restaurant_screen.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; +import 'dine_in_details_screen.dart'; +import 'dine_in_restaurant_list_screen.dart'; + +class DineInScreen extends StatelessWidget { + const DineInScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: DineInController(), + builder: (controller) { + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + expandedHeight: Responsive.height(38, context), + floating: true, + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Row( + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.arrow_back, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ], + ), + flexibleSpace: FlexibleSpaceBar( + background: Stack( + children: [ + Image.asset("assets/images/dine_in_bg.png", fit: BoxFit.fill, width: Responsive.width(100, context)), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Dine-In Reservations".tr, + style: TextStyle(fontSize: 24, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, color: isDark ? AppThemeData.grey900 : AppThemeData.grey900), + ), + Text( + "Book a table at your favorite restaurant and enjoy a delightful dining experience.".tr, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey900 : AppThemeData.grey900), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ]; + }, + body: + controller.isLoading.value + ? Constant.loader() + : Constant.isZoneAvailable == false || controller.allNearestRestaurant.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/location.gif", height: 120), + const SizedBox(height: 12), + Text("No Store Found in Your Area".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "Currently, there are no available store in your zone. Try changing your location to find nearby options.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Change Zone".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LocationPermissionScreen()); + }, + ), + ], + ), + ) + : SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + titleView(isDark, "Explore the Categories".tr, () { + Get.to(const ViewAllCategoryDineInScreen()); + }), + const SizedBox(height: 10), + CategoryView(controller: controller), + const SizedBox(height: 28), + ], + ), + ), + controller.newArrivalRestaurantList.isEmpty + ? const SizedBox() + : Container( + decoration: const BoxDecoration(image: DecorationImage(image: AssetImage("assets/images/ic_new_arrival_dinein.png"), fit: BoxFit.cover)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "New Arrivals".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + InkWell( + onTap: () { + Get.to(const DineInRestaurantListScreen(), arguments: {"vendorList": controller.newArrivalRestaurantList, "title": "New Arrival"}); + }, + child: Text( + "View all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 16), + NewArrival(controller: controller), + ], + ), + ), + ), + controller.bannerBottomModel.isEmpty + ? const SizedBox() + : Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), child: BannerBottomView(controller: controller)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + controller.isPopular.value = true; + }, + child: Container( + decoration: + controller.isPopular.value == false + ? null + : ShapeDecoration(color: AppThemeData.grey900, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "Popular Stores".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: () { + controller.isPopular.value = false; + }, + child: Container( + decoration: + controller.isPopular.value == true + ? null + : ShapeDecoration(color: AppThemeData.grey900, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "All Stores".tr, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + color: + controller.isPopular.value == true + ? isDark + ? AppThemeData.grey400 + : AppThemeData.grey500 + : isDark + ? AppThemeData.primary300 + : AppThemeData.primary300, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: controller.isPopular.value ? PopularRestaurant(controller: controller) : AllRestaurant(controller: controller), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Row titleView(isDark, String name, Function()? onPress) { + return Row( + children: [ + Expanded(child: Text(name, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900))), + InkWell( + onTap: () { + onPress!(); + }, + child: Text("View all".tr, textAlign: TextAlign.center, style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300)), + ), + ], + ); + } +} + +class PopularRestaurant extends StatelessWidget { + final DineInController controller; + + const PopularRestaurant({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemCount: controller.popularRestaurantList.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.popularRestaurantList[index]; + return InkWell( + onTap: () { + Get.to(const DineInDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle(color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 18, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ); + } +} + +class AllRestaurant extends StatelessWidget { + final DineInController controller; + + const AllRestaurant({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemCount: controller.allNearestRestaurant.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.allNearestRestaurant[index]; + return InkWell( + onTap: () { + Get.to(const DineInDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle(color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 18, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ); + } +} + +class NewArrival extends StatelessWidget { + final DineInController controller; + + const NewArrival({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return SizedBox( + height: Responsive.height(24, context), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: controller.newArrivalRestaurantList.length >= 10 ? 10 : controller.newArrivalRestaurantList.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.newArrivalRestaurantList[index]; + return InkWell( + onTap: () { + Get.to(const DineInDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + width: Responsive.width(55, context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: vendorModel.photo.toString(), fit: BoxFit.cover, height: Responsive.height(100, context), width: Responsive.width(100, context)), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, 1.00), end: const Alignment(0, -1), colors: [Colors.black.withOpacity(0), AppThemeData.grey900]), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 5), + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 16, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + Row( + children: [ + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 10), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + const SizedBox(width: 20), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg"), + const SizedBox(width: 10), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ], + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} + +class CategoryView extends StatelessWidget { + final DineInController controller; + + const CategoryView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return SizedBox( + height: 124, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemCount: controller.vendorCategoryModel.length, + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryModel[index]; + return InkWell( + onTap: () { + Get.to(const CategoryRestaurantScreen(), arguments: {"vendorCategoryModel": vendorCategoryModel, "dineIn": true}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: SizedBox( + width: 78, + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, strokeAlign: BorderSide.strokeAlignOutside, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), + borderRadius: BorderRadius.circular(100), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 60, height: 60, child: ClipOval(child: NetworkImageWidget(imageUrl: vendorCategoryModel.photo.toString(), fit: BoxFit.cover))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Text( + '${vendorCategoryModel.title}', + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class BannerBottomView extends StatelessWidget { + final DineInController controller; + + const BannerBottomView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 150, + child: PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageBottomController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.bannerBottomModel.length, + padEnds: false, + pageSnapping: true, + onPageChanged: (value) { + controller.currentBottomPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + BannerModel bannerModel = controller.bannerBottomModel[index]; + return InkWell( + onTap: () async { + if (bannerModel.redirect_type == "store") { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(bannerModel.redirect_id.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "product") { + ShowToastDialog.showLoader("Please wait...".tr); + ProductModel? productModel = await FireStoreUtils.getProductById(bannerModel.redirect_id.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel!.vendorID.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "external_link") { + final uri = Uri.parse(bannerModel.redirect_id.toString()); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + ShowToastDialog.showToast("Could not launch".tr); + } + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 14), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(12)), child: NetworkImageWidget(imageUrl: bannerModel.photo.toString(), fit: BoxFit.cover)), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.bannerBottomModel.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentBottomPage.value == index ? AppThemeData.primary300 : Colors.black12), + ), + ); + }), + ), + ), + ], + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/dine_in_screeen/view_all_category_dine_in_screen.dart b/lib/screen_ui/multi_vendor_service/dine_in_screeen/view_all_category_dine_in_screen.dart new file mode 100644 index 0000000..6d27058 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/dine_in_screeen/view_all_category_dine_in_screen.dart @@ -0,0 +1,104 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/view_all_category_controller.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../home_screen/category_restaurant_screen.dart'; + +class ViewAllCategoryDineInScreen extends StatelessWidget { + const ViewAllCategoryDineInScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ViewAllCategoryController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + "Categories".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + ), + ), + ), + body: controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4, childAspectRatio: 3.5 / 6, crossAxisSpacing: 6), + itemCount: controller.vendorCategoryModel.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryModel[index]; + return InkWell( + onTap: () { + Get.to(const CategoryRestaurantScreen(), arguments: {"vendorCategoryModel": vendorCategoryModel, "dineIn": true}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + strokeAlign: BorderSide.strokeAlignOutside, + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + ), + borderRadius: BorderRadius.circular(100), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 60, + height: 60, + child: ClipOval( + child: NetworkImageWidget( + imageUrl: vendorCategoryModel.photo.toString(), + fit: BoxFit.cover, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Text( + '${vendorCategoryModel.title}', + textAlign: TextAlign.center, + maxLines: 2, + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.medium, + ), + ), + ) + ], + ), + ), + ), + ); + }, + ), + ), + ); + }); + } +} diff --git a/lib/screen_ui/multi_vendor_service/edit_profile_screen/edit_profile_screen.dart b/lib/screen_ui/multi_vendor_service/edit_profile_screen/edit_profile_screen.dart new file mode 100644 index 0000000..06a0df5 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/edit_profile_screen/edit_profile_screen.dart @@ -0,0 +1,161 @@ +import 'dart:io'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/edit_profile_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import '../../../controllers/theme_controller.dart'; + +class EditProfileScreen extends StatelessWidget { + const EditProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: EditProfileController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(centerTitle: false, titleSpacing: 0, backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Profile Information".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "View and update your personal details, contact information, and preferences.".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 20), + Center( + child: Stack( + children: [ + controller.profileImage.isEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.asset(Constant.userPlaceHolder, height: Responsive.width(24, context), width: Responsive.width(24, context), fit: BoxFit.cover), + ) + : Constant().hasValidUrl(controller.profileImage.value) == false + ? ClipRRect( + borderRadius: BorderRadius.circular(60), + child: Image.file(File(controller.profileImage.value), height: Responsive.width(24, context), width: Responsive.width(24, context), fit: BoxFit.cover), + ) + : ClipRRect( + borderRadius: BorderRadius.circular(60), + child: NetworkImageWidget( + fit: BoxFit.cover, + imageUrl: controller.profileImage.value, + height: Responsive.width(24, context), + width: Responsive.width(24, context), + errorWidget: Image.asset(Constant.userPlaceHolder, fit: BoxFit.cover, height: Responsive.width(24, context), width: Responsive.width(24, context)), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: InkWell( + onTap: () { + buildBottomSheet(context, controller); + }, + child: SvgPicture.asset("assets/icons/ic_edit.svg"), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(child: TextFieldWidget(title: 'First Name'.tr, controller: controller.firstNameController.value, hintText: 'First Name'.tr)), + const SizedBox(width: 10), + Expanded(child: TextFieldWidget(title: 'Last Name'.tr, controller: controller.lastNameController.value, hintText: 'Last Name'.tr)), + ], + ), + TextFieldWidget(title: 'Email'.tr, textInputType: TextInputType.emailAddress, controller: controller.emailController.value, hintText: 'Email'.tr, enable: false), + TextFieldWidget(title: 'Phone Number'.tr, textInputType: TextInputType.emailAddress, controller: controller.phoneNumberController.value, hintText: 'Phone Number'.tr, enable: false), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Save Details".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + controller.saveData(); + }, + ), + ), + ), + ); + }, + ); + } + + Future buildBottomSheet(BuildContext context, EditProfileController controller) { + return showModalBottomSheet( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return SizedBox( + height: Responsive.height(22, context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding(padding: const EdgeInsets.only(top: 15), child: Text("please select".tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600))), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton(onPressed: () => controller.pickFile(source: ImageSource.camera), icon: const Icon(Icons.camera_alt, size: 32)), + Padding(padding: const EdgeInsets.only(top: 3), child: Text("camera".tr, style: const TextStyle())), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton(onPressed: () => controller.pickFile(source: ImageSource.gallery), icon: const Icon(Icons.photo_library_sharp, size: 32)), + Padding(padding: const EdgeInsets.only(top: 3), child: Text("gallery".tr, style: const TextStyle())), + ], + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/favourite_screens/favourite_screen.dart b/lib/screen_ui/multi_vendor_service/favourite_screens/favourite_screen.dart new file mode 100644 index 0000000..47a2d40 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/favourite_screens/favourite_screen.dart @@ -0,0 +1,629 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/favourite_controller.dart'; +import 'package:customer/models/favourite_item_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import '../../../controllers/theme_controller.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/restaurant_image_view.dart'; +import '../../auth_screens/login_screen.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class FavouriteScreen extends StatelessWidget { + const FavouriteScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: FavouriteController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Text( + "Your Favourites, All in One Place".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + ), + //SvgPicture.asset("assets/images/ic_favourite.svg"), + ], + ), + ), + const SizedBox(height: 20), + Expanded( + child: + Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/login.gif", height: 120), + const SizedBox(height: 12), + Text( + "Please Log In to Continue".tr, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold), + ), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + controller.favouriteRestaurant.value = true; + }, + child: Container( + decoration: + controller.favouriteRestaurant.value == false + ? null + : ShapeDecoration(color: AppThemeData.grey900, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "Favourite Store".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: () { + controller.favouriteRestaurant.value = false; + }, + child: Container( + decoration: + controller.favouriteRestaurant.value == true + ? null + : ShapeDecoration(color: AppThemeData.grey900, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "Favourite Item".tr, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + color: + controller.favouriteRestaurant.value == true + ? isDark + ? AppThemeData.grey400 + : AppThemeData.grey500 + : isDark + ? AppThemeData.primary300 + : AppThemeData.primary300, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: + controller.favouriteRestaurant.value + ? controller.favouriteVendorList.isEmpty + ? Constant.showEmptyView(message: "Favourite Store not found.".tr) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + scrollDirection: Axis.vertical, + itemCount: controller.favouriteVendorList.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.favouriteVendorList[index]; + return InkWell( + onTap: () { + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + // Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel( + restaurantId: vendorModel.id, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + controller.favouriteVendorList.removeAt(index); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel( + restaurantId: vendorModel.id, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.success300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle( + fontSize: 14, + color: AppThemeData.success600, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset( + "assets/icons/ic_star.svg", + colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn), + ), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset( + "assets/icons/ic_map_distance.svg", + colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn), + ), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ) + : controller.favouriteFoodList.isEmpty + ? Constant.showEmptyView(message: "Favourite Item not found.".tr) + : ListView.builder( + itemCount: controller.favouriteFoodList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + ProductModel productModel = controller.favouriteFoodList[index]; + return FutureBuilder( + future: getPrice(productModel), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Constant.loader(); + } else { + if (snapshot.hasError) { + return Center(child: Text('${"error".tr}: ${snapshot.error}')); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + Map map = snapshot.data!; + String price = map['price']; + String disPrice = map['disPrice']; + return InkWell( + onTap: () async { + await FireStoreUtils.getVendorById(productModel.vendorID.toString()).then((value) { + if (value != null) { + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": value}); + + // Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": value}); + } + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + productModel.nonveg == true + ? SvgPicture.asset("assets/icons/ic_nonveg.svg") + : SvgPicture.asset("assets/icons/ic_veg.svg"), + const SizedBox(width: 5), + Text( + productModel.nonveg == true ? "Non Veg.".tr : "Pure veg.".tr, + style: TextStyle( + color: productModel.nonveg == true ? AppThemeData.danger300 : AppThemeData.success400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 5), + Text( + productModel.name.toString(), + style: TextStyle( + fontSize: 18, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + double.parse(disPrice) <= 0 + ? Text( + Constant.amountShow(amount: price), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ) + : Row( + children: [ + Text( + Constant.amountShow(amount: disPrice), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: price), + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.lineThrough, + decorationColor: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + color: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Row( + children: [ + SvgPicture.asset( + "assets/icons/ic_star.svg", + colorFilter: const ColorFilter.mode(AppThemeData.warning300, BlendMode.srcIn), + ), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: productModel.reviewsCount!.toStringAsFixed(0), reviewSum: productModel.reviewsSum.toString())} (${productModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + Text( + "${productModel.description}", + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget( + imageUrl: productModel.photo.toString(), + fit: BoxFit.cover, + height: Responsive.height(16, context), + width: Responsive.width(34, context), + ), + Container( + height: Responsive.height(16, context), + width: Responsive.width(34, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteItemList.where((p0) => p0.productId == productModel.id).isNotEmpty) { + FavouriteItemModel favouriteModel = FavouriteItemModel( + productId: productModel.id, + storeId: productModel.vendorID, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteItemList.removeWhere((item) => item.productId == productModel.id); + controller.favouriteFoodList.removeAt(index); + await FireStoreUtils.removeFavouriteItem(favouriteModel); + } else { + FavouriteItemModel favouriteModel = FavouriteItemModel( + productId: productModel.id, + storeId: productModel.vendorID, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteItemList.add(favouriteModel); + await FireStoreUtils.setFavouriteItem(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteItemList.where((p0) => p0.productId == productModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + } + }, + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Future> getPrice(ProductModel productModel) async { + String price = "0.0"; + String disPrice = "0.0"; + List selectedVariants = []; + List selectedIndexVariants = []; + List selectedIndexArray = []; + + print("=======>"); + print(productModel.price); + print(productModel.disPrice); + + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel.vendorID.toString()); + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + selectedVariants.add(productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString()); + selectedIndexVariants.add('${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}'); + selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + price = Constant.productCommissionPrice(vendorModel!, productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0'); + disPrice = Constant.productCommissionPrice(vendorModel, '0'); + } + } else { + price = Constant.productCommissionPrice(vendorModel!, productModel.price.toString()); + disPrice = Constant.productCommissionPrice(vendorModel, productModel.disPrice.toString()); + } + + return {'price': price, 'disPrice': disPrice}; + } +} diff --git a/lib/screen_ui/multi_vendor_service/forgot_password_screen/forgot_password_screen.dart b/lib/screen_ui/multi_vendor_service/forgot_password_screen/forgot_password_screen.dart new file mode 100644 index 0000000..52fa366 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/forgot_password_screen/forgot_password_screen.dart @@ -0,0 +1,61 @@ +import 'package:customer/controllers/forgot_password_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; + +class ForgotPasswordScreen extends StatelessWidget { + const ForgotPasswordScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ForgotPasswordController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Forgot Password".tr, style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 22, fontFamily: AppThemeData.semiBold)), + Text("No worries!! We’ll send you reset instructions".tr, style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.regular)), + const SizedBox(height: 32), + TextFieldWidget( + title: 'Email Address'.tr, + controller: controller.emailEditingController.value, + hintText: 'Enter email address'.tr, + prefix: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset("assets/icons/ic_mail.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey300 : AppThemeData.grey600, BlendMode.srcIn)), + ), + ), + const SizedBox(height: 32), + RoundedButtonFill( + title: "Forgot Password".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + if (controller.emailEditingController.value.text.trim().isEmpty) { + ShowToastDialog.showToast("Please enter valid email".tr); + } else { + controller.forgotPassword(); + } + }, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/gift_card/gift_card_screen.dart b/lib/screen_ui/multi_vendor_service/gift_card/gift_card_screen.dart new file mode 100644 index 0000000..685fc0b --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/gift_card/gift_card_screen.dart @@ -0,0 +1,330 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/gift_card_controller.dart'; +import 'package:customer/models/gift_cards_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/gift_card/redeem_gift_card_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/gift_card/select_gift_payment_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; +import 'history_gift_card.dart'; + +class GiftCardScreen extends StatelessWidget { + const GiftCardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: GiftCardController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + "Customize Gift Card".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + actions: [ + InkWell( + onTap: () { + Get.to(const HistoryGiftCard()); + }, + child: SvgPicture.asset("assets/icons/ic_history.svg"), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + Get.to(const RedeemGiftCardScreen()); + }, + child: SvgPicture.asset("assets/icons/ic_redeem.svg"), + ), + const SizedBox(width: 10), + ], + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: Responsive.height(22, context), + child: PageView.builder( + itemCount: controller.giftCardList.length, + onPageChanged: (value) { + controller.selectedPageIndex.value = value; + controller.selectedGiftCard.value = controller.giftCardList[controller.selectedPageIndex.value]; + + controller.messageController.value.text = controller.giftCardList[controller.selectedPageIndex.value].message.toString(); + }, + scrollDirection: Axis.horizontal, + controller: controller.pageController, + itemBuilder: (context, index) { + GiftCardsModel giftCardModel = controller.giftCardList[index]; + return InkWell( + onTap: () { + controller.selectedGiftCard.value = giftCardModel; + controller.messageController.value.text = controller.selectedGiftCard.value.message.toString(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), border: Border.all(color: AppThemeData.primary300)), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget(imageUrl: giftCardModel.image.toString(), width: Responsive.width(80, context), fit: BoxFit.cover), + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 20), + TextFieldWidget( + title: 'Choose an amount'.tr, + controller: controller.amountController.value, + hintText: 'Enter gift card amount'.tr, + textInputType: const TextInputType.numberWithOptions(signed: true, decimal: true), + textInputAction: TextInputAction.done, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9]'))], + prefix: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Text( + Constant.currencyModel!.symbol.tr, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontSize: 18), + ), + ), + onchange: (value) { + controller.selectedAmount.value = value; + }, + ), + SizedBox( + height: 40, + child: ListView.builder( + itemCount: controller.amountList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Obx( + () => InkWell( + onTap: () { + controller.selectedAmount.value = controller.amountList[index]; + controller.amountController.value.text = controller.amountList[index]; + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(40)), + border: Border.all( + color: + controller.selectedAmount == controller.amountList[index] + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey400 + : AppThemeData.grey200, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Center( + child: Text( + Constant.amountShow(amount: controller.amountList[index]), + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 40), + TextFieldWidget(title: 'Add Message (Optional)'.tr, controller: controller.messageController.value, hintText: 'Add message here....'.tr, maxLine: 6), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Continue".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + if (controller.amountController.value.text.isNotEmpty) { + if (Constant.userModel == null) { + ShowToastDialog.showToast("Please log in to the application. You are not logged in.".tr); + } else { + giftCardBottomSheet(context, controller); + } + } else { + ShowToastDialog.showToast("Please enter Amount".tr); + } + }, + ), + ), + ), + ); + }, + ); + } + + Future giftCardBottomSheet(BuildContext context, GiftCardController controller) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(30))), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: + (context) => FractionallySizedBox( + heightFactor: 0.7, + child: StatefulBuilder( + builder: (context1, setState) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Obx( + () => Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 20), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget(imageUrl: controller.selectedGiftCard.value.image.toString(), height: Responsive.height(20, context), width: Responsive.width(100, context)), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Container( + padding: const EdgeInsets.all(8), + decoration: ShapeDecoration(color: AppThemeData.ecommerce50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: Text( + 'Complete payment and share this e-gift card with loved ones using any app'.tr, + style: TextStyle(color: AppThemeData.ecommerce300, fontSize: 14, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Bill Details".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Sub Total".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow(amount: controller.amountController.value.text), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Grand Total".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow(amount: controller.amountController.value.text), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 20), + Center( + child: Text( + "${'Gift Card expire'.tr} ${controller.selectedGiftCard.value.expiryDay} ${'days after purchase'.tr}".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey500 : AppThemeData.grey400), + ), + ), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "${'Pay'.tr} ${Constant.amountShow(amount: controller.amountController.value.text)}", + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + Get.off(const SelectGiftPaymentScreen()); + }, + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/gift_card/history_gift_card.dart b/lib/screen_ui/multi_vendor_service/gift_card/history_gift_card.dart new file mode 100644 index 0000000..5a34585 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/gift_card/history_gift_card.dart @@ -0,0 +1,198 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/history_gift_card_controller.dart'; +import 'package:customer/models/gift_cards_order_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../widget/my_separator.dart'; + +class HistoryGiftCard extends StatelessWidget { + const HistoryGiftCard({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: HistoryGiftCardController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: + controller.giftCardsOrderList.isEmpty + ? Constant.showEmptyView(message: "Purchased Gift card not found".tr) + : ListView.builder( + itemCount: controller.giftCardsOrderList.length, + shrinkWrap: true, + itemBuilder: (context, index) { + GiftCardsOrderModel giftCardOrderModel = controller.giftCardsOrderList[index]; + return Container( + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + giftCardOrderModel.giftTitle.toString(), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + ), + Text( + Constant.amountShow(amount: giftCardOrderModel.price.toString()), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 10), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: Text( + "Gift Code".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + ), + Text( + giftCardOrderModel.giftCode.toString().replaceAllMapped(RegExp(r".{4}"), (match) => "${match.group(0)} "), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 5), + Row( + children: [ + Expanded( + child: Text( + "Gift Pin".tr, + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + ), + giftCardOrderModel.isPasswordShow == true + ? Text( + giftCardOrderModel.giftPin.toString(), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ) + : Text( + "****", + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 10), + giftCardOrderModel.isPasswordShow == true + ? InkWell( + onTap: () { + controller.updateList(index); + controller.update(); + }, + child: const Icon(Icons.visibility_off), + ) + : InkWell( + onTap: () { + controller.updateList(index); + controller.update(); + }, + child: const Icon(Icons.remove_red_eye), + ), + ], + ), + const SizedBox(height: 10), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 10), + Row( + children: [ + InkWell( + onTap: () { + controller.share( + giftCardOrderModel.giftCode.toString(), + giftCardOrderModel.giftPin.toString(), + giftCardOrderModel.message.toString(), + giftCardOrderModel.price.toString(), + giftCardOrderModel.expireDate!, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Share'.tr, + style: TextStyle( + color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + fontSize: 14, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + height: 0.11, + ), + ), + const SizedBox(width: 4), + const Icon(Icons.share), + ], + ), + ), + ), + const Expanded(child: SizedBox()), + Text( + giftCardOrderModel.redeem == true ? "Redeemed".tr : "Not Redeem".tr, + style: TextStyle( + fontSize: 16, + color: giftCardOrderModel.redeem == true ? AppThemeData.success400 : AppThemeData.danger300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/gift_card/redeem_gift_card_screen.dart b/lib/screen_ui/multi_vendor_service/gift_card/redeem_gift_card_screen.dart new file mode 100644 index 0000000..a67bf7e --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/gift_card/redeem_gift_card_screen.dart @@ -0,0 +1,143 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/dash_board_controller.dart'; +import 'package:customer/controllers/redeem_gift_card_controller.dart'; +import 'package:customer/models/gift_cards_order_model.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import '../../../controllers/theme_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../dash_board_screens/dash_board_screen.dart'; + +class RedeemGiftCardScreen extends StatelessWidget { + const RedeemGiftCardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RedeemGiftCardController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: InkWell( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Redeem Gift Card".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "Enter your gift card code to enjoy discounts and special offers on your orders.".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 20), + TextFieldWidget( + title: 'Gift Code'.tr, + controller: controller.giftCodeController.value, + hintText: 'Enter gift code'.tr, + textInputType: TextInputType.number, + prefix: Padding(padding: const EdgeInsets.all(10), child: SvgPicture.asset("assets/icons/ic_gift_code.svg")), + ), + TextFieldWidget( + title: 'Gift Pin'.tr, + controller: controller.giftPinController.value, + hintText: 'Enter gift pin'.tr, + textInputType: TextInputType.number, + prefix: Padding(padding: const EdgeInsets.all(10), child: SvgPicture.asset("assets/icons/ic_gift_pin.svg")), + ), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Redeem".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + if (controller.giftCodeController.value.text.isEmpty) { + ShowToastDialog.showToast("Please Enter Gift Code".tr); + } else if (controller.giftPinController.value.text.isEmpty) { + ShowToastDialog.showToast("Please Enter Gift Pin".tr); + } else { + ShowToastDialog.showLoader("Please wait...".tr); + await FireStoreUtils.checkRedeemCode(controller.giftCodeController.value.text.replaceAll(" ", "")).then((value) async { + if (value != null) { + GiftCardsOrderModel giftCodeModel = value; + if (giftCodeModel.redeem == true) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Gift voucher already redeemed".tr); + } else if (giftCodeModel.giftPin != controller.giftPinController.value.text) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Gift Pin Invalid".tr); + } else if (giftCodeModel.expireDate!.toDate().isBefore(DateTime.now())) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Gift Voucher expire".tr); + } else { + giftCodeModel.redeem = true; + + WalletTransactionModel transactionModel = WalletTransactionModel( + id: Constant.getUuid(), + amount: double.parse(giftCodeModel.price.toString()), + date: Timestamp.now(), + paymentMethod: "Wallet", + transactionUser: "user", + userId: FireStoreUtils.getCurrentUid(), + isTopup: true, + note: "Gift Voucher", + paymentStatus: "success", + ); + + await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + if (value == true) { + await FireStoreUtils.updateUserWallet(amount: giftCodeModel.price.toString(), userId: FireStoreUtils.getCurrentUid()).then((value) async { + await FireStoreUtils.sendTopUpMail(paymentMethod: "Gift Voucher", amount: giftCodeModel.price.toString(), tractionId: transactionModel.id.toString()); + await FireStoreUtils.placeGiftCardOrder(giftCodeModel).then((value) { + ShowToastDialog.closeLoader(); + if (Constant.walletSetting == true) { + Get.offAll(const DashBoardScreen()); + DashBoardController controller = Get.put(DashBoardController()); + controller.selectedIndex.value = 2; + } + ShowToastDialog.showToast("Voucher redeem successfully".tr); + }); + }); + } + }); + } + } else { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Invalid Gift Code".tr); + } + }); + } + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/gift_card/select_gift_payment_screen.dart b/lib/screen_ui/multi_vendor_service/gift_card/select_gift_payment_screen.dart new file mode 100644 index 0000000..bbcf508 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/gift_card/select_gift_payment_screen.dart @@ -0,0 +1,222 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/gift_card_controller.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../wallet_screen/wallet_screen.dart'; + +class SelectGiftPaymentScreen extends StatelessWidget { + const SelectGiftPaymentScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: GiftCardController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Payment Option".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Preferred Payment".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shadows: const [BoxShadow(color: Color(0x07000000), blurRadius: 20, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + ], + ), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shadows: const [BoxShadow(color: Color(0x07000000), blurRadius: 20, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.flutterWaveModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.paytmModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + ], + ), + ), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20))), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Pay Now".tr, + height: 5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.amountController.value.text, context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.amountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + controller.placeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.amountController.value.text), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.amountController.value.text, orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + }, + ), + ), + ), + ); + }, + ); + } + + Obx cardDecoration(GiftCardController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: controller.userModel.value.walletAmount == null ? '0.0' : controller.userModel.value.walletAmount.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/category_restaurant_screen.dart b/lib/screen_ui/multi_vendor_service/home_screen/category_restaurant_screen.dart new file mode 100644 index 0000000..a311418 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/category_restaurant_screen.dart @@ -0,0 +1,198 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/category_restaurant_controller.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../widget/restaurant_image_view.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class CategoryRestaurantScreen extends StatelessWidget { + const CategoryRestaurantScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: CategoryRestaurantController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, centerTitle: false, titleSpacing: 0), + body: + controller.isLoading.value + ? Constant.loader() + : controller.allNearestRestaurant.isEmpty + ? Constant.showEmptyView(message: "No Restaurant found".tr) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.allNearestRestaurant.length, + itemBuilder: (context, index) { + VendorModel vendorModel = controller.allNearestRestaurant[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.success300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle(fontSize: 14, color: AppThemeData.success600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/discount_restaurant_list_screen.dart b/lib/screen_ui/multi_vendor_service/home_screen/discount_restaurant_list_screen.dart new file mode 100644 index 0000000..52c3082 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/discount_restaurant_list_screen.dart @@ -0,0 +1,320 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/discount_restaurant_list_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class DiscountRestaurantListScreen extends StatelessWidget { + const DiscountRestaurantListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: DiscountRestaurantListController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + controller.title.value, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.vendorList.length, + itemBuilder: (context, index) { + VendorModel vendorModel = controller.vendorList[index]; + CouponModel offerModel = controller.couponList[index]; + return InkWell( + onTap: () { + Get.to(RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), bottomLeft: Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: vendorModel.photo.toString(), fit: BoxFit.cover, height: Responsive.height(16, context), width: Responsive.width(28, context)), + Container( + height: Responsive.height(16, context), + width: Responsive.width(28, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + Positioned( + top: 10, + left: 10, + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: Text( + "${offerModel.discountType == "Fix Price" ? Constant.currencyModel!.symbol : ""}${offerModel.discount}${offerModel.discountType == "Percentage" ? "% off".toUpperCase().tr : " off".toUpperCase().tr}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ], + ), + const SizedBox(height: 5), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.location_on, size: 18, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600), + const SizedBox(width: 5), + Expanded( + child: Text( + vendorModel.location.toString(), + style: TextStyle( + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + fontSize: 12, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ), + ], + ), + const SizedBox(height: 5), + Container( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + child: DottedBorder( + options: RoundedRectDottedBorderOptions( + radius: const Radius.circular(6), + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + strokeWidth: 1, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2), + child: Text( + "${offerModel.code}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } + + // vhhv(){ + // return Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Stack( + // children: [ + // ClipRRect( + // borderRadius: const BorderRadius.only(topLeft: Radius.circular(16),topRight: Radius.circular(16)), + // child: Stack( + // children: [ + // RestaurantImageView( + // vendorModel: vendorModel, + // ), + // Container( + // height: Responsive.height(20, context), + // width: Responsive.width(100, context), + // decoration: BoxDecoration( + // gradient: LinearGradient( + // begin: const Alignment(-0.00, -1.00), + // end: const Alignment(0, 1), + // colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + // ), + // ), + // ), + // ], + // ), + // ), + // Transform.translate( + // offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.end, + // crossAxisAlignment: CrossAxisAlignment.end, + // children: [ + // Container( + // decoration: ShapeDecoration( + // color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + // ), + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + // child: Row( + // children: [ + // SvgPicture.asset( + // "assets/icons/ic_star.svg", + // colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn), + // ), + // const SizedBox( + // width: 5, + // ), + // Text( + // "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + // style: TextStyle( + // color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + // fontFamily: AppThemeData.semiBold, + // fontWeight: FontWeight.w600, + // ), + // ), + // ], + // ), + // ), + // ), + // const SizedBox( + // width: 10, + // ), + // Container( + // decoration: ShapeDecoration( + // color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + // ), + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + // child: Row( + // children: [ + // SvgPicture.asset( + // "assets/icons/ic_map_distance.svg", + // colorFilter: const ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn), + // ), + // const SizedBox( + // width: 5, + // ), + // Text( + // "${Constant.getDistance( + // lat1: vendorModel.latitude.toString(), + // lng1: vendorModel.longitude.toString(), + // lat2: Constant.selectedLocation.location!.latitude.toString(), + // lng2: Constant.selectedLocation.location!.longitude.toString(), + // )} ${Constant.distanceType}", + // style: TextStyle( + // color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + // fontFamily: AppThemeData.semiBold, + // fontWeight: FontWeight.w600, + // ), + // ), + // ], + // ), + // ), + // ), + // ], + // ), + // ) + // ], + // ), + // const SizedBox( + // height: 15, + // ), + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // vendorModel.title.toString(), + // textAlign: TextAlign.start, + // maxLines: 1, + // style: TextStyle( + // fontSize: 18, + // overflow: TextOverflow.ellipsis, + // fontFamily: AppThemeData.semiBold, + // color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + // ), + // ), + // Text( + // vendorModel.location.toString(), + // textAlign: TextAlign.start, + // maxLines: 1, + // style: TextStyle( + // overflow: TextOverflow.ellipsis, + // fontFamily: AppThemeData.medium, + // fontWeight: FontWeight.w500, + // color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + // ), + // ) + // ], + // ), + // ), + // const SizedBox( + // height: 10, + // ), + // ], + // ); + // } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/home_screen.dart b/lib/screen_ui/multi_vendor_service/home_screen/home_screen.dart new file mode 100644 index 0000000..2d361f6 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/home_screen.dart @@ -0,0 +1,2042 @@ +import 'package:badges/badges.dart' as badges; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/map_view_controller.dart'; +import 'package:customer/models/advertisement_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/restaurant_list_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/story_view.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/view_all_category_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/custom_dialog_box.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:customer/widget/osm_map/map_picker_page.dart'; +import 'package:customer/widget/place_picker/location_picker_screen.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:latlong2/latlong.dart' as location; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../controllers/food_home_controller.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../models/banner_model.dart'; +import '../../../models/story_model.dart'; +import '../../../service/database_helper.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../themes/text_field_widget.dart'; +import '../../../widget/restaurant_image_view.dart'; +import '../../../widget/video_widget.dart'; +import '../../auth_screens/login_screen.dart'; +import '../advertisement_screens/all_advertisement_screen.dart'; +import '../cart_screen/cart_screen.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; +import '../scan_qrcode_screen/scan_qr_code_screen.dart'; +import '../search_screen/search_screen.dart'; +import 'category_restaurant_screen.dart'; +import 'discount_restaurant_list_screen.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: FoodHomeController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(0.00, -3), + colors: [isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce50, isDark ? AppThemeData.surfaceDark : AppThemeData.surface], + end: const Alignment(0, 1), + ), + ), + child: + controller.isLoading.value + ? Constant.loader() + : Constant.isZoneAvailable == false || controller.allNearestRestaurant.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/location.gif", height: 120), + const SizedBox(height: 12), + Text("No Store Found in Your Area".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "Currently, there are no available store in your zone. Try changing your location to find nearby options.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Change Zone".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LocationPermissionScreen()); + }, + ), + ], + ), + ) + : Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: + controller.isListView.value == false + ? const MapView() + : Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 10), + Row( + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.arrow_back, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, size: 20), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell( + onTap: () { + Get.offAll(const LoginScreen()); + }, + child: Text( + "Login".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 12), + ), + ) + : Text( + Constant.userModel!.fullName(), + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 12), + ), + InkWell( + onTap: () async { + if (Constant.userModel != null) { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } else { + Constant.checkPermission( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + // ✅ declare it once here! + ShippingAddress shippingAddress = ShippingAddress(); + + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(); + ShowToastDialog.closeLoader(); + + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + shippingAddress.addressAs = "Home"; + shippingAddress.locality = address.toString(); + shippingAddress.location = UserLocation(latitude: lat, longitude: lng); + Constant.selectedLocation = shippingAddress; + controller.getData(); + Get.back(); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation( + latitude: selectedLocationModel.latLng!.latitude, + longitude: selectedLocationModel.latLng!.longitude, + ); + shippingAddress.locality = "Picked from Map"; // You can reverse-geocode + + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + shippingAddress.locality = currentLocation; + }); + + Constant.selectedLocation = shippingAddress; + ShowToastDialog.closeLoader(); + controller.getData(); + } + }, + context: context, + ); + } + }, + child: Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + TextSpan( + text: Constant.selectedLocation.getFullAddress(), + style: TextStyle( + fontFamily: AppThemeData.medium, + overflow: TextOverflow.ellipsis, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontSize: 14, + ), + ), + WidgetSpan(child: SvgPicture.asset("assets/icons/ic_down.svg")), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(width: 5), + Obx( + () => badges.Badge( + showBadge: cartItem.isEmpty ? false : true, + badgeContent: Text( + "${cartItem.length}", + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + badgeStyle: badges.BadgeStyle(shape: badges.BadgeShape.circle, badgeColor: AppThemeData.ecommerce300), + child: InkWell( + onTap: () async { + (await Get.to(const CartScreen())); + controller.getCartData(); + }, + child: ClipOval( + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/icons/ic_shoping_cart.svg", + colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey50 : AppThemeData.grey900, BlendMode.srcIn), + ), + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + Get.to(const SearchScreen(), arguments: {"vendorList": controller.allNearestRestaurant}); + }, + child: TextFieldWidget( + hintText: + Constant.sectionConstantModel?.name?.toLowerCase().contains('restaurants') == true + ? 'Search the restaurant, food and more...'.tr + : 'Search the store, item and more...'.tr, + controller: null, + enable: false, + prefix: Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: SvgPicture.asset("assets/icons/ic_search.svg")), + ), + ), + const SizedBox(height: 5), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + controller.storyList.isEmpty || Constant.storyEnable == false + ? const SizedBox() + : Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: StoryView(controller: controller)), + SizedBox(height: controller.storyList.isEmpty ? 0 : 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + titleView(isDark, "Explore the Categories", () { + Get.to(const ViewAllCategoryScreen()); + }), + const SizedBox(height: 10), + CategoryView(controller: controller), + ], + ), + ), + const SizedBox(height: 32), + controller.bannerModel.isEmpty ? const SizedBox() : Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: BannerView(controller: controller)), + controller.couponRestaurantList.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + titleView(isDark, "Largest Discounts", () { + Get.to( + const DiscountRestaurantListScreen(), + arguments: {"vendorList": controller.couponRestaurantList, "couponList": controller.couponList, "title": "Discounts Restaurants"}, + ); + }), + const SizedBox(height: 16), + OfferView(controller: controller), + ], + ), + ), + const SizedBox(height: 28), + controller.newArrivalRestaurantList.isEmpty + ? const SizedBox() + : Container( + decoration: const BoxDecoration(image: DecorationImage(image: AssetImage("assets/images/ic_new_arrival_bg.png"), fit: BoxFit.cover)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "New Arrivals".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + InkWell( + onTap: () { + Get.to(const RestaurantListScreen(), arguments: {"vendorList": controller.newArrivalRestaurantList, "title": "New Arrival"})?.then((v) { + controller.getFavouriteRestaurant(); + }); + }, + child: Text( + "View all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 16), + NewArrival(controller: controller), + ], + ), + ), + ), + const SizedBox(height: 20), + controller.bannerBottomModel.isEmpty + ? const SizedBox() + : Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: BannerBottomView(controller: controller)), + Visibility(visible: (Constant.isEnableAdsFeature == true && controller.advertisementList.isNotEmpty), child: const SizedBox(height: 20)), + Visibility( + visible: Constant.isEnableAdsFeature == true, + child: + controller.advertisementList.isEmpty + ? const SizedBox() + : Container( + color: AppThemeData.primary300.withAlpha(40), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "Highlights for you".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + InkWell( + onTap: () { + Get.to(AllAdvertisementScreen())?.then((value) { + controller.getFavouriteRestaurant(); + }); + }, + child: Text( + "View all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 220, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: controller.advertisementList.length >= 10 ? 10 : controller.advertisementList.length, + padding: EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return AdvertisementHomeCard(controller: controller, model: controller.advertisementList[index]); + }, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + controller.isPopular.value = true; + }, + child: Container( + decoration: + controller.isPopular.value == false + ? null + : ShapeDecoration(color: AppThemeData.grey900, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "Popular Stores".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: () { + controller.isPopular.value = false; + }, + child: Container( + decoration: + controller.isPopular.value == true + ? null + : ShapeDecoration(color: AppThemeData.grey900, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "All Stores".tr, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: AppThemeData.semiBold, + color: + controller.isPopular.value == true + ? isDark + ? AppThemeData.grey400 + : AppThemeData.grey500 + : isDark + ? AppThemeData.primary300 + : AppThemeData.primary300, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: controller.isPopular.value ? PopularRestaurant(controller: controller) : AllRestaurant(controller: controller), + ), + // controller.isPopular.value + // ? PopularRestaurant( + // controller: controller, + // ) + // : PopularRestaurant( + // controller: controller, + // ), + ], + ), + ), + ), + ], + ), + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, borderRadius: const BorderRadius.all(Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, borderRadius: const BorderRadius.all(Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + children: [ + InkWell( + onTap: () { + controller.isListView.value = true; + }, + child: ClipOval( + child: Container( + decoration: BoxDecoration(color: controller.isListView.value ? AppThemeData.primary300 : null), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/icons/ic_view_grid_list.svg", + colorFilter: ColorFilter.mode(controller.isListView.value ? AppThemeData.grey50 : AppThemeData.grey500, BlendMode.srcIn), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + controller.isListView.value = false; + controller.update(); + }, + child: ClipOval( + child: Container( + decoration: BoxDecoration(color: controller.isListView.value == false ? AppThemeData.primary300 : null), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/icons/ic_map_draw.svg", + colorFilter: ColorFilter.mode(controller.isListView.value == false ? AppThemeData.grey50 : AppThemeData.grey500, BlendMode.srcIn), + ), + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + Get.to(const ScanQrCodeScreen()); + }, + child: ClipOval( + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + child: Padding( + padding: const EdgeInsets.all(10), + child: SvgPicture.asset("assets/icons/ic_scan_code.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey400 : AppThemeData.grey500, BlendMode.srcIn)), + ), + ), + ), + ), + const SizedBox(width: 14), + Theme( + data: Theme.of(context).copyWith( + canvasColor: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, // background when dropdown opens + ), + child: DropdownButton( + isDense: false, + underline: const SizedBox(), + dropdownColor: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, + value: controller.selectedOrderTypeValue.value.tr, + icon: const Icon(Icons.keyboard_arrow_down), + items: + ['Delivery'.tr, 'TakeAway'.tr].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value.tr, style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ); + }).toList(), + onChanged: (value) async { + if (cartItem.isEmpty) { + await Preferences.setString(Preferences.foodDeliveryType, value!); + controller.selectedOrderTypeValue.value = value; + controller.getData(); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return CustomDialogBox( + title: "Alert".tr, + descriptions: "Do you really want to change the delivery option? Your cart will be empty.".tr, + positiveString: "Ok".tr, + negativeString: "Cancel".tr, + positiveClick: () async { + await Preferences.setString(Preferences.foodDeliveryType, value!); + controller.selectedOrderTypeValue.value = value; + controller.getData(); + DatabaseHelper.instance.deleteAllCartProducts(); + controller.cartProvider.clearDatabase(); + controller.getCartData(); + Get.back(); + }, + negativeClick: () { + Get.back(); + }, + img: null, + ); + }, + ); + } + }, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Row titleView(isDark, String name, Function()? onPress) { + return Row( + children: [ + Expanded(child: Text(name.tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900))), + InkWell( + onTap: () { + onPress!(); + }, + child: Text("View all".tr, textAlign: TextAlign.center, style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300)), + ), + ], + ); + } +} + +class PopularRestaurant extends StatelessWidget { + final FoodHomeController controller; + + const PopularRestaurant({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemCount: controller.popularRestaurantList.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.popularRestaurantList[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel})?.then((v) { + controller.getFavouriteRestaurant(); + }); + }, + child: Padding( + padding: EdgeInsets.only(bottom: controller.popularRestaurantList.length - 1 == index ? 60 : 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.success300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text("Free Delivery".tr, style: TextStyle(fontSize: 14, color: AppThemeData.carRent600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600)), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 18, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ); + } +} + +class AllRestaurant extends StatelessWidget { + final FoodHomeController controller; + + const AllRestaurant({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemCount: controller.allNearestRestaurant.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.allNearestRestaurant[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel})?.then((v) { + controller.getFavouriteRestaurant(); + }); + }, + child: Padding( + padding: EdgeInsets.only(bottom: controller.allNearestRestaurant.length - 1 == index ? 60 : 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.carRent300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text("Free Delivery".tr, style: TextStyle(fontSize: 14, color: AppThemeData.carRent600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600)), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 18, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ); + } +} + +class NewArrival extends StatelessWidget { + final FoodHomeController controller; + + const NewArrival({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return SizedBox( + height: Responsive.height(24, context), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: controller.newArrivalRestaurantList.length >= 10 ? 10 : controller.newArrivalRestaurantList.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.newArrivalRestaurantList[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel})?.then((v) { + controller.getFavouriteRestaurant(); + }); + }, + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + width: Responsive.width(55, context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: vendorModel.photo.toString(), fit: BoxFit.cover, height: Responsive.height(100, context), width: Responsive.width(100, context)), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, 1.00), end: const Alignment(0, -1), colors: [Colors.black.withOpacity(0), AppThemeData.grey900]), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 5), + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 16, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 4), + Text( + "Free Delivery".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + const SizedBox(width: 8), + ], + ), + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 4), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + const SizedBox(width: 8), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg"), + const SizedBox(width: 4), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ], + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} + +class AdvertisementHomeCard extends StatelessWidget { + final AdvertisementModel model; + final FoodHomeController controller; + + const AdvertisementHomeCard({super.key, required this.controller, required this.model}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(model.vendorId!); + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Container( + margin: EdgeInsets.only(right: 16), + width: Responsive.width(70, context), + decoration: BoxDecoration( + color: isDark ? AppThemeData.info600 : AppThemeData.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: isDark ? 6 : 2, spreadRadius: 0, offset: Offset(0, isDark ? 3 : 1))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + model.type == 'restaurant_promotion' + ? ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + child: NetworkImageWidget(imageUrl: model.coverImage ?? '', height: 135, width: double.infinity, fit: BoxFit.cover), + ) + : VideoAdvWidget(url: model.video ?? '', height: 135, width: double.infinity), + if (model.type != 'video_promotion' && model.vendorId != null && (model.showRating == true || model.showReview == true)) + Positioned( + bottom: 8, + right: 8, + child: FutureBuilder( + future: FireStoreUtils.getVendorById(model.vendorId!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(); + } else { + if (snapshot.hasError) { + return const SizedBox(); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + VendorModel vendorModel = snapshot.data!; + return Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + if (model.showRating == true) SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + if (model.showRating == true) const SizedBox(width: 5), + Text( + "${model.showRating == true ? Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString()) : ''} ${model.showReview == true ? '(${vendorModel.reviewsCount!.toStringAsFixed(0)})' : ''}", + style: TextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ); + } + } + }, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (model.type == 'restaurant_promotion') + ClipRRect(borderRadius: BorderRadius.circular(30), child: NetworkImageWidget(imageUrl: model.profileImage ?? '', height: 50, width: 50, fit: BoxFit.cover)), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.title ?? '', + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 14, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + Text( + model.description ?? '', + style: TextStyle(fontSize: 12, fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey400 : AppThemeData.grey600), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ), + ), + model.type == 'restaurant_promotion' + ? IconButton( + icon: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == model.vendorId).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey400 : AppThemeData.grey600, BlendMode.srcIn)), + ), + onPressed: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == model.vendorId).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: model.vendorId, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == model.vendorId); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: model.vendorId, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + controller.update(); + }, + ) + : Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), + child: Padding(padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: Icon(Icons.arrow_forward, size: 20, color: AppThemeData.primary300)), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class OfferView extends StatelessWidget { + final FoodHomeController controller; + + const OfferView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return SizedBox( + height: Responsive.height(17, context), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: controller.couponRestaurantList.length >= 15 ? 15 : controller.couponRestaurantList.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.couponRestaurantList[index]; + CouponModel offerModel = controller.couponList[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + width: Responsive.width(38, context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: vendorModel.photo.toString(), fit: BoxFit.cover, height: Responsive.height(100, context), width: Responsive.width(100, context)), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), AppThemeData.grey900]), + ), + ), + Positioned( + bottom: 5, + left: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Upto".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w900, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + Text( + "${offerModel.discountType == "Fix Price" ? Constant.currencyModel!.symbol : ""}${offerModel.discount}${offerModel.discountType == "Percentage" ? "% off".tr : "off".tr}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 5), + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 16, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle( + fontSize: 12, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + ), + ), + ], + ), + const SizedBox(width: 6), + ], + ), + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 10), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 12, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} + +class BannerView extends StatelessWidget { + final FoodHomeController controller; + + const BannerView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 150, + child: PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.bannerModel.length, + padEnds: false, + pageSnapping: true, + allowImplicitScrolling: true, + onPageChanged: (value) { + controller.currentPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + BannerModel bannerModel = controller.bannerModel[index]; + return InkWell( + onTap: () async { + if (bannerModel.redirect_type == "store") { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(bannerModel.redirect_id.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "product") { + ShowToastDialog.showLoader("Please wait...".tr); + ProductModel? productModel = await FireStoreUtils.getProductById(bannerModel.redirect_id.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel!.vendorID.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "external_link") { + final uri = Uri.parse(bannerModel.redirect_id.toString()); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + ShowToastDialog.showToast("Could not launch".tr); + } + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 14), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(12)), child: NetworkImageWidget(imageUrl: bannerModel.photo.toString(), fit: BoxFit.cover)), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.bannerModel.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentPage.value == index ? AppThemeData.primary300 : Colors.black12), + ), + ); + }), + ), + ), + ], + ); + } +} + +class BannerBottomView extends StatelessWidget { + final FoodHomeController controller; + + const BannerBottomView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 150, + child: PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageBottomController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.bannerBottomModel.length, + padEnds: false, + pageSnapping: true, + allowImplicitScrolling: true, + onPageChanged: (value) { + controller.currentBottomPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + BannerModel bannerModel = controller.bannerBottomModel[index]; + return InkWell( + onTap: () async { + if (bannerModel.redirect_type == "store") { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(bannerModel.redirect_id.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "product") { + ShowToastDialog.showLoader("Please wait...".tr); + ProductModel? productModel = await FireStoreUtils.getProductById(bannerModel.redirect_id.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel!.vendorID.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "external_link") { + final uri = Uri.parse(bannerModel.redirect_id.toString()); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + ShowToastDialog.showToast("Could not launch".tr); + } + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 14), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(12)), child: NetworkImageWidget(imageUrl: bannerModel.photo.toString(), fit: BoxFit.cover)), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.bannerBottomModel.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentBottomPage.value == index ? AppThemeData.primary300 : Colors.black12), + ), + ); + }), + ), + ), + ], + ); + } +} + +class CategoryView extends StatelessWidget { + final FoodHomeController controller; + + const CategoryView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return SizedBox( + height: 124, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemCount: controller.vendorCategoryModel.length, + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryModel[index]; + return InkWell( + onTap: () { + Get.to(const CategoryRestaurantScreen(), arguments: {"vendorCategoryModel": vendorCategoryModel, "dineIn": false}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + child: SizedBox( + width: 78, + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, strokeAlign: BorderSide.strokeAlignOutside, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), + borderRadius: BorderRadius.circular(100), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 60, height: 60, child: ClipOval(child: NetworkImageWidget(imageUrl: vendorCategoryModel.photo.toString(), fit: BoxFit.cover))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Text( + '${vendorCategoryModel.title}', + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class StoryView extends StatelessWidget { + final FoodHomeController controller; + + const StoryView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 180, + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.storyList.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + StoryModel storyModel = controller.storyList[index]; + return Padding( + padding: const EdgeInsets.only(right: 10), + child: InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => MoreStories(storyList: controller.storyList, index: index))); + }, + child: SizedBox( + width: 134, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: storyModel.videoThumbnail.toString(), fit: BoxFit.cover, height: Responsive.height(100, context), width: Responsive.width(100, context)), + Container(color: Colors.black.withOpacity(0.30)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 8), + child: FutureBuilder( + future: FireStoreUtils.getVendorById(storyModel.vendorID.toString()), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Constant.loader(); + } else { + if (snapshot.hasError) { + return Center(child: Text('${"Error".tr}: ${snapshot.error}')); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + VendorModel vendorModel = snapshot.data!; + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval(child: NetworkImageWidget(imageUrl: vendorModel.photo.toString(), width: 30, height: 30, fit: BoxFit.cover)), + const SizedBox(width: 4), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle(color: Colors.white, fontSize: 12, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700), + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg"), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum!.toStringAsFixed(0))} reviews", + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle(color: AppThemeData.warning300, fontSize: 10, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700), + ), + ], + ), + ], + ), + ), + ], + ); + } + } + }, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class MapView extends StatelessWidget { + const MapView({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: MapViewController(), + builder: (controller) { + return Stack( + children: [ + Constant.selectedMapType == "osm" + ? flutterMap.FlutterMap( + mapController: controller.osmMapController, + options: flutterMap.MapOptions( + initialCenter: location.LatLng(Constant.selectedLocation.location!.latitude ?? 0.0, Constant.selectedLocation.location!.longitude ?? 0.0), + initialZoom: 10, + ), + children: [ + flutterMap.TileLayer(urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.emart.app'), + flutterMap.MarkerLayer(markers: controller.osmMarker), + ], + ) + : GoogleMap( + mapType: MapType.terrain, + myLocationEnabled: true, + myLocationButtonEnabled: true, + zoomControlsEnabled: false, + markers: Set.of(controller.markers.values), + onMapCreated: (GoogleMapController mapController) { + controller.mapController = mapController; + }, + mapToolbarEnabled: true, + initialCameraPosition: CameraPosition( + zoom: 18, + target: + controller.homeController.allNearestRestaurant.isEmpty + ? LatLng(Constant.selectedLocation.location!.latitude ?? 45.521563, Constant.selectedLocation.location!.longitude ?? -122.677433) + : LatLng(controller.homeController.allNearestRestaurant.first.latitude ?? 45.521563, controller.homeController.allNearestRestaurant.first.longitude ?? -122.677433), + ), + ), + controller.homeController.allNearestRestaurant.isEmpty + ? Container() + : Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 80), + child: SizedBox( + height: Responsive.height(25, context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: PageView.builder( + pageSnapping: true, + controller: PageController(viewportFraction: 0.88), + onPageChanged: (value) async { + if (Constant.selectedMapType == "osm") { + controller.osmMapController.move( + location.LatLng(controller.homeController.allNearestRestaurant[value].latitude!, controller.homeController.allNearestRestaurant[value].longitude!), + 16, + ); + } else { + CameraUpdate cameraUpdate = CameraUpdate.newCameraPosition( + CameraPosition( + zoom: 18, + target: LatLng(controller.homeController.allNearestRestaurant[value].latitude!, controller.homeController.allNearestRestaurant[value].longitude!), + ), + ); + controller.mapController!.animateCamera(cameraUpdate); + } + }, + itemCount: controller.homeController.allNearestRestaurant.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + VendorModel vendorModel = controller.homeController.allNearestRestaurant[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel})?.then((v) { + controller.homeController.getFavouriteRestaurant(); + }); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: index == 0 ? 0 : 10), + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, borderRadius: const BorderRadius.all(Radius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget( + imageUrl: vendorModel.photo.toString(), + fit: BoxFit.cover, + height: Responsive.height(14, context), + width: Responsive.width(100, context), + ), + Container( + height: Responsive.height(14, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.homeController.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.homeController.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.homeController.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.homeController.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(11, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.carRent300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle(fontSize: 14, color: AppThemeData.success600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/home_screen_two.dart b/lib/screen_ui/multi_vendor_service/home_screen/home_screen_two.dart new file mode 100644 index 0000000..ef0cfed --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/home_screen_two.dart @@ -0,0 +1,1074 @@ +import 'dart:math'; + +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/food_home_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/story_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/restaurant_list_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/story_view.dart'; +import 'package:customer/screen_ui/multi_vendor_service/home_screen/view_all_category_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/custom_dialog_box.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/utils/preferences.dart'; +import 'package:customer/widget/osm_map/map_picker_page.dart'; +import 'package:customer/widget/place_picker/location_picker_screen.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../models/banner_model.dart'; +import '../../../service/database_helper.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/gradiant_text.dart'; +import '../../auth_screens/login_screen.dart'; +import '../advertisement_screens/all_advertisement_screen.dart'; +import '../cart_screen/cart_screen.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; +import '../scan_qrcode_screen/scan_qr_code_screen.dart'; +import '../search_screen/search_screen.dart'; +import 'category_restaurant_screen.dart'; +import 'discount_restaurant_list_screen.dart'; +import 'home_screen.dart'; + +class HomeScreenTwo extends StatelessWidget { + const HomeScreenTwo({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: FoodHomeController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + body: + controller.isLoading.value + ? Constant.loader() + : Constant.isZoneAvailable == false || controller.allNearestRestaurant.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/location.gif", height: 120), + const SizedBox(height: 12), + Text("No Store Found in Your Area".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "Currently, there are no available store in your zone. Try changing your location to find nearby options.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Change Zone".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LocationPermissionScreen()); + }, + ), + ], + ), + ) + : Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: + controller.isListView.value == false + ? const MapView() + : Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.arrow_back, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, size: 20), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell( + onTap: () { + Get.offAll(const LoginScreen()); + }, + child: Text( + "Login".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 12), + ), + ) + : Text( + Constant.userModel!.fullName(), + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 12), + ), + InkWell( + onTap: () async { + if (Constant.userModel != null) { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } else { + Constant.checkPermission( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + // ✅ declare once for whole method + ShippingAddress shippingAddress = ShippingAddress(); + + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(); + ShowToastDialog.closeLoader(); + + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + shippingAddress.addressAs = "Home"; + shippingAddress.locality = address.toString(); + shippingAddress.location = UserLocation(latitude: lat, longitude: lng); + Constant.selectedLocation = shippingAddress; + controller.getData(); + Get.back(); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation( + latitude: selectedLocationModel.latLng!.latitude, + longitude: selectedLocationModel.latLng!.longitude, + ); + shippingAddress.locality = "Picked from Map"; // You can reverse-geocode + + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + shippingAddress.locality = currentLocation; + }); + + Constant.selectedLocation = shippingAddress; + ShowToastDialog.closeLoader(); + controller.getData(); + } + }, + context: context, + ); + } + }, + child: Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + TextSpan( + text: Constant.selectedLocation.getFullAddress(), + style: TextStyle( + fontFamily: AppThemeData.medium, + overflow: TextOverflow.ellipsis, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontSize: 14, + ), + ), + WidgetSpan(child: SvgPicture.asset("assets/icons/ic_down.svg")), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(width: 5), + InkWell( + onTap: () async { + (await Get.to(const CartScreen())); + controller.getCartData(); + }, + child: ClipOval( + child: Container( + padding: const EdgeInsets.all(8.0), + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + child: SvgPicture.asset( + "assets/icons/ic_shoping_cart.svg", + colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey50 : AppThemeData.grey900, BlendMode.srcIn), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + Get.to(const SearchScreen(), arguments: {"vendorList": controller.allNearestRestaurant}); + }, + child: TextFieldWidget( + hintText: + Constant.sectionConstantModel?.name?.toLowerCase().contains('restaurants') == true + ? 'Search the dish, foo and more...'.tr + : 'Search the store, item and more...'.tr, + controller: null, + enable: false, + prefix: Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: SvgPicture.asset("assets/icons/ic_search.svg")), + ), + ), + const SizedBox(height: 5), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + controller.bannerModel.isEmpty ? const SizedBox() : Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: BannerView(controller: controller)), + const SizedBox(height: 20), + Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: CategoryView(controller: controller)), + controller.couponRestaurantList.isEmpty + ? const SizedBox() + : Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: Column(children: [const SizedBox(height: 20), OfferView(controller: controller)])), + controller.storyList.isEmpty || Constant.storyEnable == false + ? const SizedBox() + : Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: Column(children: [const SizedBox(height: 20), StoryView(controller: controller)])), + Visibility( + visible: Constant.isEnableAdsFeature == true, + child: + controller.advertisementList.isEmpty + ? const SizedBox() + : Column( + children: [ + const SizedBox(height: 20), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: AppThemeData.primary300.withAlpha(40)), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "Highlights for you".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + InkWell( + onTap: () { + Get.to(AllAdvertisementScreen())?.then((value) { + controller.getFavouriteRestaurant(); + }); + }, + child: Text( + "See all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 220, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: controller.advertisementList.length >= 10 ? 10 : controller.advertisementList.length, + padding: EdgeInsets.all(0), + itemBuilder: (BuildContext context, int index) { + return AdvertisementHomeCard(controller: controller, model: controller.advertisementList[index]); + }, + ), + ), + ], + ), + ), + ], + ), + ), + controller.allNearestRestaurant.isEmpty ? const SizedBox() : Column(children: [const SizedBox(height: 20), RestaurantView(controller: controller)]), + ], + ), + ), + ), + ], + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, borderRadius: const BorderRadius.all(Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, borderRadius: const BorderRadius.all(Radius.circular(30))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + children: [ + InkWell( + onTap: () { + controller.isListView.value = true; + }, + child: ClipOval( + child: Container( + decoration: BoxDecoration(color: controller.isListView.value ? AppThemeData.primary300 : null), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/icons/ic_view_grid_list.svg", + colorFilter: ColorFilter.mode(controller.isListView.value ? AppThemeData.grey50 : AppThemeData.grey500, BlendMode.srcIn), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + controller.isListView.value = false; + }, + child: ClipOval( + child: Container( + decoration: BoxDecoration(color: controller.isListView.value == false ? AppThemeData.primary300 : null), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/icons/ic_map_draw.svg", + colorFilter: ColorFilter.mode(controller.isListView.value == false ? AppThemeData.grey50 : AppThemeData.grey500, BlendMode.srcIn), + ), + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + Get.to(const ScanQrCodeScreen()); + }, + child: ClipOval( + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + child: Padding( + padding: const EdgeInsets.all(10), + child: SvgPicture.asset("assets/icons/ic_scan_code.svg", colorFilter: ColorFilter.mode(isDark ? AppThemeData.grey400 : AppThemeData.grey500, BlendMode.srcIn)), + ), + ), + ), + ), + const SizedBox(width: 14), + DropdownButton( + isDense: false, + underline: const SizedBox(), + value: controller.selectedOrderTypeValue.value.tr, + icon: const Icon(Icons.keyboard_arrow_down), + items: + ['Delivery'.tr, 'TakeAway'.tr].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value.tr, style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ); + }).toList(), + onChanged: (value) async { + if (cartItem.isEmpty) { + await Preferences.setString(Preferences.foodDeliveryType, value!); + controller.selectedOrderTypeValue.value = value; + controller.getData(); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return CustomDialogBox( + title: "Alert".tr, + descriptions: "Do you really want to change the delivery option? Your cart will be empty.".tr, + positiveString: "Ok".tr, + negativeString: "Cancel".tr, + positiveClick: () async { + await Preferences.setString(Preferences.foodDeliveryType, value!); + controller.selectedOrderTypeValue.value = value; + controller.getData(); + DatabaseHelper.instance.deleteAllCartProducts(); + controller.cartProvider.clearDatabase(); + controller.getCartData(); + Get.back(); + }, + negativeClick: () { + Get.back(); + }, + img: null, + ); + }, + ); + } + }, + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class CategoryView extends StatelessWidget { + final FoodHomeController controller; + + const CategoryView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded(child: Text("Our Categories".tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 18))), + InkWell( + onTap: () { + Get.to(const ViewAllCategoryScreen()); + }, + child: Text( + "See all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 14), + ), + ), + ], + ), + GradientText( + 'Best Servings Food'.tr, + style: TextStyle(fontSize: 24, fontFamily: 'Inter Tight', fontWeight: FontWeight.w800), + gradient: LinearGradient(colors: [Color(0xFF3961F1), Color(0xFF11D0EA)]), + ), + ], + ), + ), + const SizedBox(height: 10), + GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4, childAspectRatio: 5 / 6), + itemCount: controller.vendorCategoryModel.length >= 8 ? 8 : controller.vendorCategoryModel.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryModel[index]; + return InkWell( + onTap: () { + Get.to(const CategoryRestaurantScreen(), arguments: {"vendorCategoryModel": vendorCategoryModel, "dineIn": false}); + }, + child: Column( + children: [ + ClipOval(child: SizedBox(width: 60, height: 60, child: NetworkImageWidget(imageUrl: vendorCategoryModel.photo.toString(), fit: BoxFit.cover))), + Text( + "${vendorCategoryModel.title}", + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 12), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ); + } +} + +class OfferView extends StatelessWidget { + final FoodHomeController controller; + + const OfferView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded(child: Text("Large Discounts".tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 18))), + InkWell( + onTap: () { + Get.to( + const DiscountRestaurantListScreen(), + arguments: {"vendorList": controller.couponRestaurantList, "couponList": controller.couponList, "title": "Discounts Restaurants"}, + ); + }, + child: Text( + "See all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 14), + ), + ), + ], + ), + GradientText( + 'Save Upto 50% Off'.tr, + style: TextStyle(fontSize: 24, fontFamily: 'Inter Tight', fontWeight: FontWeight.w800), + gradient: LinearGradient(colors: [Color(0xFF39F1C5), Color(0xFF97EA11)]), + ), + ], + ), + ), + const SizedBox(height: 10), + SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.width * 0.32, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: controller.couponRestaurantList.length >= 15 ? 15 : controller.couponRestaurantList.length, + itemBuilder: (context, index) { + VendorModel vendorModel = controller.couponRestaurantList[index]; + CouponModel offerModel = controller.couponList[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + width: Responsive.width(34, context), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: vendorModel.photo.toString(), fit: BoxFit.cover, height: Responsive.height(100, context), width: Responsive.width(100, context)), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), AppThemeData.grey900]), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 18, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + const SizedBox(height: 5), + RoundedButtonFill( + title: + "${offerModel.discountType == "Fix Price" ? Constant.currencyModel!.symbol : ""}${offerModel.discount}${offerModel.discountType == "Percentage" ? "% off".tr : "off".tr}", + color: Colors.primaries[Random().nextInt(Colors.primaries.length)], + textColor: AppThemeData.grey50, + width: 20, + height: 3.5, + onPress: () async {}, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } +} + +class BannerView extends StatelessWidget { + final FoodHomeController controller; + + const BannerView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 150, + child: PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.bannerModel.length, + padEnds: false, + pageSnapping: true, + onPageChanged: (value) { + controller.currentPage.value = value; + }, + itemBuilder: (BuildContext context, int index) { + BannerModel bannerModel = controller.bannerModel[index]; + return InkWell( + onTap: () async { + if (bannerModel.redirect_type == "store") { + ShowToastDialog.showLoader("Please wait...".tr); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(bannerModel.redirect_id.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "product") { + ShowToastDialog.showLoader("Please wait...".tr); + ProductModel? productModel = await FireStoreUtils.getProductById(bannerModel.redirect_id.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel!.vendorID.toString()); + + ShowToastDialog.closeLoader(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + } else if (bannerModel.redirect_type == "external_link") { + final uri = Uri.parse(bannerModel.redirect_id.toString()); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + ShowToastDialog.showToast("Could not launch".tr); + } + } + }, + child: Padding( + padding: const EdgeInsets.only(right: 14), + child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(12)), child: NetworkImageWidget(imageUrl: bannerModel.photo.toString(), fit: BoxFit.cover)), + ), + ); + }, + ), + ); + } +} + +class StoryView extends StatelessWidget { + final FoodHomeController controller; + + const StoryView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Container( + height: Responsive.height(32, context), + decoration: const BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(20)), image: DecorationImage(image: AssetImage("assets/images/story_bg.png"), fit: BoxFit.cover)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [Expanded(child: Text("Stories".tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontSize: 18)))]), + GradientText( + 'Best Food Stories Ever'.tr, + style: TextStyle(fontSize: 24, fontFamily: 'Inter Tight', fontWeight: FontWeight.w800), + gradient: LinearGradient(colors: [Color(0xFFF1C839), Color(0xFFEA1111)]), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.storyList.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + StoryModel storyModel = controller.storyList[index]; + return Padding( + padding: const EdgeInsets.only(right: 10), + child: InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => MoreStories(storyList: controller.storyList, index: index))); + }, + child: SizedBox( + width: 134, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: storyModel.videoThumbnail.toString(), fit: BoxFit.cover, height: Responsive.height(100, context), width: Responsive.width(100, context)), + Container(color: Colors.black.withOpacity(0.30)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 8), + child: FutureBuilder( + future: FireStoreUtils.getVendorById(storyModel.vendorID.toString()), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Constant.loader(); + } else { + if (snapshot.hasError) { + return Center(child: Text('${"Error".tr}: ${snapshot.error}')); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + VendorModel vendorModel = snapshot.data!; + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval(child: NetworkImageWidget(imageUrl: vendorModel.photo.toString(), width: 30, height: 30, fit: BoxFit.cover)), + const SizedBox(width: 4), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle(color: Colors.white, fontSize: 12, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700), + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg"), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum!.toStringAsFixed(0))} ${'reviews'.tr}", + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle(color: AppThemeData.warning300, fontSize: 10, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700), + ), + ], + ), + ], + ), + ), + ], + ); + } + } + }, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 10), + ], + ), + ); + } +} + +class RestaurantView extends StatelessWidget { + final FoodHomeController controller; + + const RestaurantView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: Row( + children: [ + Expanded(child: Text("Best Restaurants".tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 18))), + InkWell( + onTap: () { + Get.to(const RestaurantListScreen(), arguments: {"vendorList": controller.allNearestRestaurant, "title": "Best Restaurants"}); + }, + child: Text( + "See all".tr, + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 14), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemCount: controller.allNearestRestaurant.length, + itemBuilder: (BuildContext context, int index) { + VendorModel vendorModel = controller.allNearestRestaurant[index]; + List tempList = []; + List discountAmountTempList = []; + for (var element in controller.couponList) { + if (vendorModel.id == element.vendorID && element.expiresAt!.toDate().isAfter(DateTime.now())) { + tempList.add(element); + discountAmountTempList.add(double.parse(element.discount.toString())); + } + } + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget(height: Responsive.height(14, context), width: Responsive.width(30, context), imageUrl: vendorModel.photo.toString(), fit: BoxFit.cover), + Container( + height: Responsive.height(14, context), + width: Responsive.width(30, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + discountAmountTempList.isEmpty + ? const SizedBox() + : Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Upto".tr, + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w900, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + Text( + discountAmountTempList.reduce(min).toString() + "% OFF".tr, + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle(fontSize: 18, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + const SizedBox(height: 5), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg", width: 18), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + const SizedBox(width: 10), + ], + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SvgPicture.asset("assets/icons/ic_star.svg", width: 18, colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + ), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Icon(Icons.circle, size: 5, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/restaurant_list_screen.dart b/lib/screen_ui/multi_vendor_service/home_screen/restaurant_list_screen.dart new file mode 100644 index 0000000..7e5c18f --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/restaurant_list_screen.dart @@ -0,0 +1,229 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/restaurant_list_controller.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import '../../../controllers/theme_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../service/fire_store_utils.dart'; +import '../../../widget/restaurant_image_view.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class RestaurantListScreen extends StatelessWidget { + const RestaurantListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RestaurantListController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + controller.title.value, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.vendorSearchList.length, + itemBuilder: (context, index) { + VendorModel vendorModel = controller.vendorSearchList[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel})?.then((v) { + controller.getFavouriteRestaurant(); + }); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == vendorModel.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: vendorModel.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == vendorModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.success300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle(fontSize: 14, color: AppThemeData.carRent600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/story_view.dart b/lib/screen_ui/multi_vendor_service/home_screen/story_view.dart new file mode 100644 index 0000000..3b9d6b2 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/story_view.dart @@ -0,0 +1,171 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/story_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/widget/story_view/controller/story_controller.dart'; +import 'package:customer/widget/story_view/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../widget/story_view/widgets/story_view.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +// ignore: must_be_immutable +class MoreStories extends StatefulWidget { + final List storyList; + int index; + + MoreStories({super.key, required this.index, required this.storyList}); + + @override + MoreStoriesState createState() => MoreStoriesState(); +} + +class MoreStoriesState extends State { + StoryController storyController = StoryController(); + + @override + void dispose() { + storyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GestureDetector( + onHorizontalDragEnd: (details) { + // Swipe to next story + if (details.primaryVelocity != null && details.primaryVelocity! < 0) { + if (widget.index < widget.storyList.length - 1) { + setState(() { + storyController.dispose(); + storyController = StoryController(); + }); + setState(() { + widget.index++; + }); + } else { + Navigator.pop(context); + } + } + + // Swipe to previous story + if (details.primaryVelocity != null && details.primaryVelocity! > 0) { + if (widget.index > 0) { + setState(() { + storyController.dispose(); + storyController = StoryController(); + }); + setState(() { + widget.index--; + }); + } + } + }, + child: Stack( + children: [ + StoryView( + key: ValueKey(widget.index), + storyItems: + List.generate(widget.storyList[widget.index].videoUrl.length, (i) { + return StoryItem.pageVideo(widget.storyList[widget.index].videoUrl[i], controller: storyController); + }).toList(), + onComplete: () { + debugPrint("--------->"); + debugPrint(widget.storyList.length.toString()); + debugPrint(widget.index.toString()); + if (widget.storyList.length - 1 != widget.index) { + setState(() { + widget.index = widget.index + 1; + }); + } else { + Navigator.pop(context); + } + }, + progressPosition: ProgressPosition.top, + repeat: true, + controller: storyController, + onVerticalSwipeComplete: (direction) { + if (direction == Direction.down) { + Navigator.pop(context); + } + }, + ), + Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top + 30, left: 16, right: 16), + child: FutureBuilder( + future: FireStoreUtils.getVendorById(widget.storyList[widget.index].vendorID.toString()), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return SizedBox(); + } else { + if (snapshot.hasError) { + return Center(child: Text('${"Error".tr}: ${snapshot.error}')); + return Center(child: Text('Error: ${snapshot.error}')); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + VendorModel vendorModel = snapshot.data!; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipOval(child: NetworkImageWidget(imageUrl: vendorModel.photo.toString(), width: 50, height: 50, fit: BoxFit.cover)), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle(color: Colors.white, fontSize: 16, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700), + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg"), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount.toString(), reviewSum: vendorModel.reviewsSum.toString())} ${'reviews'.tr}", + textAlign: TextAlign.center, + maxLines: 1, + style: const TextStyle(color: AppThemeData.warning300, fontSize: 12, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700), + ), + ], + ), + ], + ), + ), + InkWell( + onTap: () async { + Get.back(); + }, + child: Container( + padding: const EdgeInsets.all(10.0), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(30), color: Colors.grey), + child: SvgPicture.asset("assets/icons/ic_close.svg", colorFilter: ColorFilter.mode(AppThemeData.grey800, BlendMode.srcIn)), + ), + ), + ], + ), + ); + } + } + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/home_screen/view_all_category_screen.dart b/lib/screen_ui/multi_vendor_service/home_screen/view_all_category_screen.dart new file mode 100644 index 0000000..49cc84a --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/home_screen/view_all_category_screen.dart @@ -0,0 +1,82 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/view_all_category_controller.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import 'category_restaurant_screen.dart'; + +class ViewAllCategoryScreen extends StatelessWidget { + const ViewAllCategoryScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ViewAllCategoryController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Categories".tr, style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500)), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4, childAspectRatio: 3.5 / 6, crossAxisSpacing: 6), + itemCount: controller.vendorCategoryModel.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryModel[index]; + return InkWell( + onTap: () { + Get.to(const CategoryRestaurantScreen(), arguments: {"vendorCategoryModel": vendorCategoryModel, "dineIn": false}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, strokeAlign: BorderSide.strokeAlignOutside, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), + borderRadius: BorderRadius.circular(100), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 60, height: 60, child: ClipOval(child: NetworkImageWidget(imageUrl: vendorCategoryModel.photo.toString(), fit: BoxFit.cover))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Text( + '${vendorCategoryModel.title}', + textAlign: TextAlign.center, + maxLines: 2, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 12), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/order_list_screen/live_tracking_screen.dart b/lib/screen_ui/multi_vendor_service/order_list_screen/live_tracking_screen.dart new file mode 100644 index 0000000..0e2a4a8 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/order_list_screen/live_tracking_screen.dart @@ -0,0 +1,56 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/live_tracking_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart' as flutterMap; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as gmap; +import '../../../controllers/theme_controller.dart'; + +class LiveTrackingScreen extends StatelessWidget { + const LiveTrackingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: LiveTrackingController(), + builder: (controller) { + if (controller.isLoading.value) { + return Scaffold(body: Constant.loader()); + } + + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar(backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, title: Text("Live Tracking".tr), centerTitle: false), + body: + Constant.selectedMapType == 'osm' + ? flutterMap.FlutterMap( + mapController: controller.osmMapController, + options: flutterMap.MapOptions(initialCenter: controller.driverCurrent.value, initialZoom: 14), + children: [ + flutterMap.TileLayer(urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.emart.customer'), + if (controller.routePoints.isNotEmpty) flutterMap.PolylineLayer(polylines: [flutterMap.Polyline(points: controller.routePoints, strokeWidth: 5.0, color: Colors.blue)]), + flutterMap.MarkerLayer(markers: controller.orderModel.value.id == null ? [] : controller.osmMarkers), + ], + ) + : gmap.GoogleMap( + onMapCreated: (gmap.GoogleMapController mapController) { + controller.mapController = mapController; + }, + myLocationEnabled: true, + zoomControlsEnabled: false, + polylines: Set.of(controller.polyLines.values), + markers: Set.of(controller.markers.values), + initialCameraPosition: gmap.CameraPosition( + zoom: 14, + target: gmap.LatLng(controller.driverUserModel.value.location?.latitude ?? 0.0, controller.driverUserModel.value.location?.longitude ?? 0.0), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/order_list_screen/order_details_screen.dart b/lib/screen_ui/multi_vendor_service/order_list_screen/order_details_screen.dart new file mode 100644 index 0000000..0434d25 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/order_list_screen/order_details_screen.dart @@ -0,0 +1,1056 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/order_details_controller.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/tax_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:timelines_plus/timelines_plus.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/my_separator.dart'; +import '../chat_screens/chat_screen.dart'; +import '../rate_us_screen/rate_product_screen.dart'; +import 'live_tracking_screen.dart'; + +class OrderDetailsScreen extends StatelessWidget { + const OrderDetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: OrderDetailsController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Order Details".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${'Order'.tr} ${Constant.orderId(orderId: controller.orderModel.value.id.toString())}".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + RoundedButtonFill( + title: controller.orderModel.value.status.toString().tr, + color: Constant.statusColor(status: controller.orderModel.value.status.toString()), + width: 32, + height: 4.5, + textColor: Constant.statusText(status: controller.orderModel.value.status.toString()), + onPress: () async {}, + ), + ], + ), + const SizedBox(height: 14), + controller.orderModel.value.takeAway == true + ? Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${controller.orderModel.value.vendor!.title}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + Text( + "${controller.orderModel.value.vendor!.location}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600), + ), + ], + ), + ), + controller.orderModel.value.status == Constant.orderPlaced || + controller.orderModel.value.status == Constant.orderRejected || + controller.orderModel.value.status == Constant.orderCompleted + ? const SizedBox() + : InkWell( + onTap: () { + Constant.makePhoneCall(controller.orderModel.value.vendor!.phonenumber.toString()); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + const SizedBox(width: 10), + controller.orderModel.value.status == Constant.orderPlaced || + controller.orderModel.value.status == Constant.orderRejected || + controller.orderModel.value.status == Constant.orderCompleted + ? const SizedBox() + : InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.orderModel.value.authorID.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(controller.orderModel.value.vendor!.author.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(restaurantUser!.vendorID.toString()); + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": vendorModel!.title, + "orderId": controller.orderModel.value.id, + "restaurantId": restaurantUser.id, + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": vendorModel.photo, + "token": restaurantUser.fcmToken, + "chatType": "restaurant", + }, + ); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ), + ), + ) + : Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Timeline.tileBuilder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + theme: TimelineThemeData( + nodePosition: 0, + // indicatorPosition: 0, + ), + builder: TimelineTileBuilder.connected( + contentsAlign: ContentsAlign.basic, + indicatorBuilder: (context, index) { + return SvgPicture.asset("assets/icons/ic_location.svg"); + }, + connectorBuilder: (context, index, connectorType) { + return const DashedLineConnector(color: AppThemeData.grey300, gap: 3); + }, + contentsBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: + index == 0 + ? Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${controller.orderModel.value.vendor!.title}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + Text( + "${controller.orderModel.value.vendor!.location}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600), + ), + ], + ), + ), + controller.orderModel.value.status == Constant.orderPlaced || + controller.orderModel.value.status == Constant.orderRejected || + controller.orderModel.value.status == Constant.orderCompleted + ? const SizedBox() + : InkWell( + onTap: () { + Constant.makePhoneCall(controller.orderModel.value.vendor!.phonenumber.toString()); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + const SizedBox(width: 10), + controller.orderModel.value.status == Constant.orderPlaced || + controller.orderModel.value.status == Constant.orderRejected || + controller.orderModel.value.status == Constant.orderCompleted + ? const SizedBox() + : InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.orderModel.value.authorID.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(controller.orderModel.value.vendor!.author.toString()); + VendorModel? vendorModel = await FireStoreUtils.getVendorById(restaurantUser!.vendorID.toString()); + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": vendorModel!.title, + "orderId": controller.orderModel.value.id, + "restaurantId": restaurantUser.id, + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": vendorModel.photo, + "token": restaurantUser.fcmToken, + "chatType": "restaurant", + }, + ); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${controller.orderModel.value.address!.addressAs}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + Text( + controller.orderModel.value.address!.getFullAddress(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600), + ), + ], + ), + ); + }, + itemCount: 2, + ), + ), + controller.orderModel.value.status == Constant.orderRejected + ? const SizedBox() + : Column( + children: [ + Padding(padding: const EdgeInsets.symmetric(vertical: 10), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)), + controller.orderModel.value.status == Constant.orderCompleted && controller.orderModel.value.driver != null + ? Row( + children: [ + SvgPicture.asset("assets/icons/ic_check_small.svg"), + const SizedBox(width: 5), + Text( + controller.orderModel.value.driver!.fullName(), + textAlign: TextAlign.right, + style: TextStyle( + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + const SizedBox(width: 5), + Text( + "Order Delivered.".tr, + textAlign: TextAlign.right, + style: TextStyle( + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ], + ) + : controller.orderModel.value.status == Constant.orderAccepted || controller.orderModel.value.status == Constant.driverPending + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/icons/ic_timer.svg"), + const SizedBox(width: 5), + Expanded( + child: Text( + "${'Your Order has been Preparing and assign to the driver'.tr}\n${'Preparation Time'.tr} ${controller.orderModel.value.estimatedTimeToPrepare}" + .tr, + textAlign: TextAlign.start, + style: TextStyle( + color: isDark ? AppThemeData.warning400 : AppThemeData.warning400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + ], + ) + : controller.orderModel.value.driver != null + ? Row( + children: [ + ClipOval( + child: NetworkImageWidget( + imageUrl: controller.orderModel.value.author!.profilePictureURL.toString(), + fit: BoxFit.cover, + height: Responsive.height(5, context), + width: Responsive.width(10, context), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.orderModel.value.driver!.fullName().toString(), + textAlign: TextAlign.start, + style: TextStyle( + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + Text( + controller.orderModel.value.driver!.email.toString(), + textAlign: TextAlign.start, + style: TextStyle( + color: isDark ? AppThemeData.success400 : AppThemeData.success400, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ], + ), + ), + InkWell( + onTap: () { + Constant.makePhoneCall(controller.orderModel.value.driver!.phoneNumber.toString()); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.orderModel.value.authorID.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(controller.orderModel.value.driverID.toString()); + + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": restaurantUser!.fullName(), + "orderId": controller.orderModel.value.id, + "restaurantId": restaurantUser.id, + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": restaurantUser.profilePictureURL, + "token": restaurantUser.fcmToken, + "chatType": "Driver", + }, + ); + }, + child: Container( + width: 42, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ) + : const SizedBox(), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + const SizedBox(height: 14), + Text( + "Your Order".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: controller.orderModel.value.products!.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + CartProductModel cartProductModel = controller.orderModel.value.products![index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(14)), + child: Stack( + children: [ + NetworkImageWidget( + imageUrl: cartProductModel.photo.toString(), + height: Responsive.height(8, context), + width: Responsive.width(16, context), + fit: BoxFit.cover, + ), + Container( + height: Responsive.height(8, context), + width: Responsive.width(16, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "${cartProductModel.name}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ), + Text( + "x ${cartProductModel.quantity}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + double.parse(cartProductModel.discountPrice == null || cartProductModel.discountPrice!.isEmpty ? "0.0" : cartProductModel.discountPrice.toString()) <= 0 + ? Text( + Constant.amountShow(amount: cartProductModel.price), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ) + : Row( + children: [ + Text( + Constant.amountShow(amount: cartProductModel.discountPrice.toString()), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: cartProductModel.price), + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.lineThrough, + decorationColor: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + color: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Align( + alignment: Alignment.centerRight, + child: RoundedButtonFill( + title: "Rate us".tr, + height: 3.8, + width: 20, + color: isDark ? AppThemeData.warning300 : AppThemeData.warning300, + textColor: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + onPress: () async { + Get.to(const RateProductScreen(), arguments: {"orderModel": controller.orderModel.value, "productId": cartProductModel.id}); + }, + ), + ), + ], + ), + ), + ], + ), + cartProductModel.variantInfo == null || cartProductModel.variantInfo!.variantOptions!.isEmpty + ? Container() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Variants".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + const SizedBox(height: 5), + Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: + List.generate(cartProductModel.variantInfo!.variantOptions!.length, (i) { + return Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Text( + "${cartProductModel.variantInfo!.variantOptions!.keys.elementAt(i)} : ${cartProductModel.variantInfo!.variantOptions![cartProductModel.variantInfo!.variantOptions!.keys.elementAt(i)]}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey500 : AppThemeData.grey400), + ), + ), + ); + }).toList(), + ), + ], + ), + ), + cartProductModel.extras == null || cartProductModel.extras!.isEmpty + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "Addons".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow( + amount: (double.parse(cartProductModel.extrasPrice.toString()) * double.parse(cartProductModel.quantity.toString())).toString(), + ), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontSize: 16), + ), + ], + ), + Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: + List.generate(cartProductModel.extras!.length, (i) { + return Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Text( + cartProductModel.extras![i].toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.grey500 : AppThemeData.grey400), + ), + ), + ); + }).toList(), + ), + ], + ), + ], + ); + }, + separatorBuilder: (context, index) { + return Padding(padding: const EdgeInsets.symmetric(vertical: 10), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)); + }, + ), + ), + ), + const SizedBox(height: 14), + // if (controller.orderModel.value.takeAway != true && + // controller.orderModel.value.status == + // Constant.orderCompleted) + // Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // "Delivery Man".tr, + // textAlign: TextAlign.start, + // style: TextStyle( + // fontFamily: AppThemeData.semiBold, + // fontSize: 16, + // color: isDark + // ? AppThemeData.grey50 + // : AppThemeData.grey900, + // ), + // ), + // const SizedBox( + // height: 10, + // ), + // const SizedBox( + // height: 14, + // ), + // ], + // ), + Text( + "Bill Details".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Item totals".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow(amount: controller.subTotal.value.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + controller.orderModel.value.takeAway == true + ? const SizedBox() + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Delivery Fee".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + (controller.orderModel.value.vendor?.isSelfDelivery == true) + ? Text('Free Delivery'.tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.regular, color: AppThemeData.success400, fontSize: 16)) + : Text( + Constant.amountShow( + amount: + controller.orderModel.value.deliveryCharge == null || controller.orderModel.value.deliveryCharge!.isEmpty + ? "0.0" + : controller.orderModel.value.deliveryCharge.toString(), + ), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Coupon Discount".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + "- (${Constant.amountShow(amount: controller.orderModel.value.discount.toString())})", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.danger300 : AppThemeData.danger300, fontSize: 16), + ), + ], + ), + controller.orderModel.value.specialDiscount != null && controller.orderModel.value.specialDiscount!['special_discount'] != null + ? Column( + children: [ + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Special Discount".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + "- (${Constant.amountShow(amount: controller.specialDiscountAmount.value.toString())})", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.danger300 : AppThemeData.danger300, fontSize: 16), + ), + ], + ), + ], + ) + : const SizedBox(), + const SizedBox(height: 10), + controller.orderModel.value.takeAway == true || controller.orderModel.value.vendor?.isSelfDelivery == true + ? const SizedBox() + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Delivery Tips".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ], + ), + ), + Text( + Constant.amountShow(amount: controller.orderModel.value.tipAmount.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 10), + ListView.builder( + itemCount: controller.orderModel.value.taxSetting!.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + TaxModel taxModel = controller.orderModel.value.taxSetting![index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "${taxModel.title.toString()} (${taxModel.type == "fix" ? Constant.amountShow(amount: taxModel.tax) : "${taxModel.tax}%"})", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow( + amount: + Constant.calculateTax( + amount: + (controller.subTotal.value - double.parse(controller.orderModel.value.discount.toString()) - controller.specialDiscountAmount.value) + .toString(), + taxModel: taxModel, + ).toString(), + ), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "To Pay".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.amountShow(amount: controller.totalAmount.value.toString()), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 14), + Text( + "Order Details".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Delivery type".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + controller.orderModel.value.takeAway == true + ? "TakeAway".tr + : controller.orderModel.value.scheduleTime == null + ? "Standard".tr + : "Schedule".tr, + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.medium, + color: + controller.orderModel.value.scheduleTime != null + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey50 + : AppThemeData.grey900, + fontSize: 16, + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Payment Method".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + controller.orderModel.value.paymentMethod.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Date and Time".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + Constant.timestampToDateTime(controller.orderModel.value.createdAt!), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Phone Number".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ], + ), + ), + Text( + controller.orderModel.value.author!.phoneNumber.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ), + const SizedBox(height: 20), + controller.orderModel.value.notes == null || controller.orderModel.value.notes!.isEmpty + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Remarks".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Text( + controller.orderModel.value.notes.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ), + ), + ], + ), + ], + ), + ), + ), + bottomNavigationBar: + controller.orderModel.value.status == Constant.orderShipped || + controller.orderModel.value.status == Constant.orderInTransit || + controller.orderModel.value.status == Constant.orderCompleted + ? Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: + controller.orderModel.value.status == Constant.orderShipped || controller.orderModel.value.status == Constant.orderInTransit + ? RoundedButtonFill( + title: "Track Order".tr, + height: 5.5, + color: AppThemeData.warning300, + textColor: AppThemeData.grey900, + onPress: () async { + Get.to(const LiveTrackingScreen(), arguments: {"orderModel": controller.orderModel.value}); + }, + ) + : RoundedButtonFill( + title: "Reorder".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + for (var element in controller.orderModel.value.products!) { + controller.addToCart(cartProductModel: element); + ShowToastDialog.showToast("Item Added In a cart".tr); + } + }, + ), + ), + ) + : const SizedBox(), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/order_list_screen/order_screen.dart b/lib/screen_ui/multi_vendor_service/order_list_screen/order_screen.dart new file mode 100644 index 0000000..3cacd3a --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/order_list_screen/order_screen.dart @@ -0,0 +1,345 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/order_controller.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/order_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/my_separator.dart'; +import '../../auth_screens/login_screen.dart'; +import 'live_tracking_screen.dart'; +import 'order_details_screen.dart'; + +class OrderScreen extends StatelessWidget { + const OrderScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: OrderController(), + builder: (controller) { + return Scaffold( + body: Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: + controller.isLoading.value + ? Constant.loader() + : Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/login.gif", height: 120), + const SizedBox(height: 12), + Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : DefaultTabController( + length: 5, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "My Order".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "Keep track your delivered, In Progress and Rejected item all in just one place.".tr, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120))), + child: TabBar( + indicator: BoxDecoration( + borderRadius: BorderRadius.circular(50), // Creates border + color: AppThemeData.primary300, + ), + labelColor: AppThemeData.grey50, + isScrollable: true, + tabAlignment: TabAlignment.start, + indicatorWeight: 0.5, + unselectedLabelColor: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + dividerColor: Colors.transparent, + indicatorSize: TabBarIndicatorSize.tab, + tabs: [ + Padding(padding: const EdgeInsets.symmetric(horizontal: 18), child: Tab(text: 'All'.tr)), + Tab(text: 'In Progress'.tr), + Tab(text: 'Delivered'.tr), + Tab(text: 'Cancelled'.tr), + Tab(text: 'Rejected'.tr), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: TabBarView( + children: [ + controller.allList.isEmpty + ? Constant.showEmptyView(message: "Order Not Found".tr) + : RefreshIndicator( + onRefresh: () => controller.getOrder(), + child: ListView.builder( + itemCount: controller.allList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + OrderModel orderModel = controller.allList[index]; + return itemView(isDark, context, orderModel, controller); + }, + ), + ), + controller.inProgressList.isEmpty + ? Constant.showEmptyView(message: "Order Not Found".tr) + : RefreshIndicator( + onRefresh: () => controller.getOrder(), + child: ListView.builder( + itemCount: controller.inProgressList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + OrderModel orderModel = controller.inProgressList[index]; + return itemView(isDark, context, orderModel, controller); + }, + ), + ), + controller.deliveredList.isEmpty + ? Constant.showEmptyView(message: "Order Not Found".tr) + : RefreshIndicator( + onRefresh: () => controller.getOrder(), + child: ListView.builder( + itemCount: controller.deliveredList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + OrderModel orderModel = controller.deliveredList[index]; + return itemView(isDark, context, orderModel, controller); + }, + ), + ), + controller.cancelledList.isEmpty + ? Constant.showEmptyView(message: "Order Not Found".tr) + : RefreshIndicator( + onRefresh: () => controller.getOrder(), + child: ListView.builder( + itemCount: controller.cancelledList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + OrderModel orderModel = controller.cancelledList[index]; + return itemView(isDark, context, orderModel, controller); + }, + ), + ), + controller.rejectedList.isEmpty + ? Constant.showEmptyView(message: "Order Not Found".tr) + : RefreshIndicator( + onRefresh: () => controller.getOrder(), + child: ListView.builder( + itemCount: controller.rejectedList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + OrderModel orderModel = controller.rejectedList[index]; + return itemView(isDark, context, orderModel, controller); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Padding itemView(isDark, BuildContext context, OrderModel orderModel, OrderController controller) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: orderModel.vendor!.photo.toString(), fit: BoxFit.cover, height: Responsive.height(10, context), width: Responsive.width(20, context)), + Container( + height: Responsive.height(10, context), + width: Responsive.width(20, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, 1.00), end: const Alignment(0, -1), colors: [Colors.black.withOpacity(0), AppThemeData.grey900]), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + orderModel.status.toString(), + textAlign: TextAlign.right, + style: TextStyle(color: Constant.statusColor(status: orderModel.status.toString()), fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500, fontSize: 12), + ), + const SizedBox(height: 5), + Text( + orderModel.vendor!.title.toString(), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 5), + Text( + Constant.timestampToDateTime(orderModel.createdAt!), + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + ListView.builder( + itemCount: orderModel.products!.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + CartProductModel cartProduct = orderModel.products![index]; + return Row( + children: [ + Expanded( + child: Text( + "${cartProduct.quantity} x ${cartProduct.name.toString()}", + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ), + Text( + Constant.amountShow( + amount: + double.parse(cartProduct.discountPrice.toString()) <= 0 + ? (double.parse('${cartProduct.price ?? 0}') * double.parse('${cartProduct.quantity ?? 0}')).toString() + : (double.parse('${cartProduct.discountPrice ?? 0}') * double.parse('${cartProduct.quantity ?? 0}')).toString(), + ), + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + ], + ); + }, + ), + Padding(padding: const EdgeInsets.symmetric(vertical: 14), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)), + Row( + children: [ + orderModel.status == Constant.orderCompleted + ? Expanded( + child: InkWell( + onTap: () { + for (var element in orderModel.products!) { + controller.addToCart(cartProductModel: element); + ShowToastDialog.showToast("Item Added In a cart".tr); + } + }, + child: Text( + "Reorder".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, fontSize: 16), + ), + ), + ) + : orderModel.status == Constant.orderShipped || orderModel.status == Constant.orderInTransit + ? Expanded( + child: InkWell( + onTap: () { + Get.to(const LiveTrackingScreen(), arguments: {"orderModel": orderModel}); + }, + child: Text( + "Track Order".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, fontSize: 16), + ), + ), + ) + : const SizedBox(), + Expanded( + child: InkWell( + onTap: () { + Get.to(const OrderDetailsScreen(), arguments: {"orderModel": orderModel}); + // Get.off(const OrderPlacingScreen(), arguments: {"orderModel": orderModel}); + }, + child: Text( + "View Details".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, fontSize: 16), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart b/lib/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart new file mode 100644 index 0000000..12f17fc --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/profile_screen/profile_screen.dart @@ -0,0 +1,379 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/my_profile_controller.dart'; +import 'package:customer/screen_ui/on_demand_service/provider_inbox_screen.dart'; +import 'package:customer/screen_ui/on_demand_service/worker_inbox_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/custom_dialog_box.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:in_app_review/in_app_review.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../auth_screens/login_screen.dart'; +import '../cashback_screen/cashback_offers_list.dart'; +import '../change langauge/change_language_screen.dart'; +import '../chat_screens/driver_inbox_screen.dart'; +import '../chat_screens/restaurant_inbox_screen.dart'; +import '../dine_in_booking/dine_in_booking_screen.dart'; +import '../dine_in_screeen/dine_in_screen.dart'; +import '../edit_profile_screen/edit_profile_screen.dart'; +import '../gift_card/gift_card_screen.dart'; +import '../refer_friend_screen/refer_friend_screen.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../terms_and_condition/terms_and_condition_screen.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Scaffold( + body: Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: MyProfileController(), + builder: (controller) { + return controller.isLoading.value + ? Constant.loader() + : Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "My Profile".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "Manage your personal information, preferences, and settings all in one place.".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 20), + Text( + "General Information".tr, + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Column( + children: [ + Constant.userModel == null + ? const SizedBox() + : cardDecoration(isDark, controller, "assets/images/ic_profile.svg", "Profile Information".tr, () { + Get.to(const EditProfileScreen()); + }), + if (Constant.sectionConstantModel!.dineInActive == true) + cardDecoration(isDark, controller, "assets/images/ic_dinin.svg", "Dine-In".tr, () { + Get.to(const DineInScreen()); + }), + cardDecoration(isDark, controller, "assets/images/ic_gift.svg", "Gift Card".tr, () { + Get.to(const GiftCardScreen()); + }), + if (Constant.isCashbackActive == true) + cardDecoration(isDark, controller, "assets/icons/ic_cashback_Offer.svg", "Cashback Offers".tr, () { + Get.to(const CashbackOffersListScreen()); + }), + ], + ), + ), + ), + const SizedBox(height: 20), + Constant.sectionConstantModel!.dineInActive == true + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Bookings Information".tr, + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Column( + children: [ + cardDecoration(isDark, controller, "assets/icons/ic_dinin_order.svg", "Dine-In Booking".tr, () { + Get.to(const DineInBookingScreen()); + }), + ], + ), + ), + ), + ], + ) + : const SizedBox(), + const SizedBox(height: 10), + Text( + "Preferences".tr, + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Column( + children: [ + cardDecoration(isDark, controller, "assets/icons/ic_change_language.svg", "Change Language".tr, () { + Get.to(const ChangeLanguageScreen()); + }), + cardDecoration(isDark, controller, "assets/icons/ic_light_dark.svg", "Dark Mode".tr, () {}), + ], + ), + ), + ), + const SizedBox(height: 10), + Text( + "Social".tr, + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Column( + children: [ + Constant.userModel == null + ? const SizedBox() + : cardDecoration(isDark, controller, "assets/icons/ic_refer.svg", "Refer a Friend".tr, () { + Get.to(const ReferFriendScreen()); + }), + cardDecoration(isDark, controller, "assets/icons/ic_share.svg", "Share app".tr, () { + Share.share( + '${'Check out Foodie, your ultimate food delivery application!'.tr} \n\n${'Google Play:'.tr} ${Constant.googlePlayLink} \n\n${'App Store:'.tr} ${Constant.appStoreLink}', + subject: 'Look what I made!'.tr, + ); + }), + cardDecoration(isDark, controller, "assets/icons/ic_rate.svg", "Rate the app".tr, () { + final InAppReview inAppReview = InAppReview.instance; + inAppReview.requestReview(); + }), + ], + ), + ), + ), + const SizedBox(height: 10), + Constant.userModel == null + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Communication".tr, + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Column( + children: [ + cardDecoration(isDark, controller, "assets/icons/ic_restaurant_chat.svg", "Store Inbox".tr, () { + Get.to(const RestaurantInboxScreen()); + }), + cardDecoration(isDark, controller, "assets/icons/ic_restaurant_driver.svg", "Driver Inbox".tr, () { + Get.to(const DriverInboxScreen()); + }), + cardDecoration(isDark, controller, "assets/icons/ic_restaurant_chat.svg", "Provider Inbox".tr, () { + Get.to(const ProviderInboxScreen()); + }), + cardDecoration(isDark, controller, "assets/icons/ic_restaurant_driver.svg", "Worker Inbox".tr, () { + Get.to(const WorkerInboxScreen()); + }), + ], + ), + ), + ), + const SizedBox(height: 10), + ], + ), + Text("Legal".tr, style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500)), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Column( + children: [ + cardDecoration(isDark, controller, "assets/icons/ic_privacy_policy.svg", "Privacy Policy".tr, () { + Get.to(const TermsAndConditionScreen(type: "privacy")); + }), + cardDecoration(isDark, controller, "assets/icons/ic_tearm_condition.svg", "Terms and Conditions".tr, () { + Get.to(const TermsAndConditionScreen(type: "termAndCondition")); + }), + ], + ), + ), + ), + const SizedBox(height: 10), + const SizedBox(height: 10), + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Column( + children: [ + Constant.userModel == null + ? cardDecoration(isDark, controller, "assets/icons/ic_logout.svg", "Log In".tr, () { + Get.offAll(const LoginScreen()); + }) + : cardDecoration(isDark, controller, "assets/icons/ic_logout.svg", "Log out".tr, () { + showDialog( + context: context, + builder: (BuildContext context) { + return CustomDialogBox( + title: "Log out".tr, + descriptions: "Are you sure you want to log out? You will need to enter your credentials to log back in.".tr, + positiveString: "Log out".tr, + negativeString: "Cancel".tr, + positiveClick: () async { + Constant.userModel!.fcmToken = ""; + await FireStoreUtils.updateUser(Constant.userModel!); + Constant.userModel = null; + await FirebaseAuth.instance.signOut(); + Get.offAll(const LoginScreen()); + }, + negativeClick: () { + Get.back(); + }, + img: Image.asset('assets/images/ic_logout.gif', height: 50, width: 50), + ); + }, + ); + }), + ], + ), + ), + ), + const SizedBox(height: 10), + Constant.userModel == null + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return CustomDialogBox( + title: "Delete Account".tr, + descriptions: "Are you sure you want to delete your account? This action is irreversible and will permanently remove all your data.".tr, + positiveString: "Delete".tr, + negativeString: "Cancel".tr, + positiveClick: () async { + ShowToastDialog.showLoader("Please wait...".tr); + await controller.deleteUserFromServer(); + await FireStoreUtils.deleteUser().then((value) { + ShowToastDialog.closeLoader(); + if (value == true) { + ShowToastDialog.showToast("Account deleted successfully".tr); + Get.offAll(const LoginScreen()); + } else { + ShowToastDialog.showToast("Contact Administrator".tr); + } + }); + }, + negativeClick: () { + Get.back(); + }, + img: Image.asset('assets/icons/delete_dialog.gif', height: 50, width: 50), + ); + }, + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_delete.svg"), + const SizedBox(width: 10), + Text( + "Delete Account".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.danger300 : AppThemeData.danger300), + ), + ], + ), + ), + ), + Center( + child: Text( + "V : ${Constant.appVersion}", + textAlign: TextAlign.center, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }), + ); + } + + Padding cardDecoration(bool isDark, MyProfileController controller, String image, String title, Function()? onPress) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: InkWell( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + onPress?.call(); + }, + child: Row( + children: [ + SvgPicture.asset(image, colorFilter: title == "Log In".tr || title == "Cashbacks".tr ? const ColorFilter.mode(AppThemeData.success500, BlendMode.srcIn) : null, height: 24, width: 24), + const SizedBox(width: 10), + Expanded( + child: Text( + title.tr, + textAlign: TextAlign.start, + style: TextStyle( + fontFamily: AppThemeData.medium, + fontSize: 16, + color: + title == "Log out".tr + ? AppThemeData.danger300 + : title == "Log In".tr + ? AppThemeData.success500 + : (isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ), + ), + title == "Dark Mode".tr + ? Transform.scale( + scale: 0.8, + child: Obx(() => CupertinoSwitch(value: controller.isDarkModeSwitch.value, activeTrackColor: AppThemeData.primary300, onChanged: controller.toggleDarkMode)), + ) + : Icon(Icons.keyboard_arrow_right, color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/rate_us_screen/rate_product_screen.dart b/lib/screen_ui/multi_vendor_service/rate_us_screen/rate_product_screen.dart new file mode 100644 index 0000000..14f1542 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/rate_us_screen/rate_product_screen.dart @@ -0,0 +1,305 @@ +import 'dart:io'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/rate_product_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../widget/my_separator.dart'; + +class RateProductScreen extends StatelessWidget { + const RateProductScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RateProductController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Rate the item".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: SingleChildScrollView( + child: Column( + children: [ + Container( + width: Responsive.width(100, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Rate for".tr, style: TextStyle(color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.medium)), + Text( + "${controller.productModel.value.name}".tr, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 18, fontFamily: AppThemeData.semiBold), + ), + const SizedBox(height: 10), + RatingBar.builder( + initialRating: controller.ratings.value, + minRating: 1, + direction: Axis.horizontal, + itemCount: 5, + itemSize: 26, + unratedColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + itemPadding: const EdgeInsets.symmetric(horizontal: 6.0), + itemBuilder: (context, _) => const Icon(Icons.star, color: AppThemeData.warning300), + onRatingUpdate: (double rate) { + controller.ratings.value = rate; + }, + ), + Padding(padding: const EdgeInsets.symmetric(vertical: 20), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)), + ListView.builder( + itemCount: controller.reviewAttributeList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Expanded( + child: Text( + controller.reviewAttributeList[index].title.toString(), + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16, fontFamily: AppThemeData.semiBold), + ), + ), + RatingBar.builder( + initialRating: + controller.ratingModel.value.id == null ? 0.0 : controller.ratingModel.value.reviewAttributes?[controller.reviewAttributeList[index].id] ?? 0.0, + minRating: 1, + direction: Axis.horizontal, + itemCount: 5, + itemSize: 18, + unratedColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + itemPadding: const EdgeInsets.symmetric(horizontal: 2.0), + itemBuilder: (context, _) => const Icon(Icons.star, color: AppThemeData.warning300), + onRatingUpdate: (double rate) { + controller.reviewAttribute.addEntries([MapEntry(controller.reviewAttributeList[index].id.toString(), rate)]); + }, + ), + ], + ), + ); + }, + ), + const SizedBox(height: 20), + DottedBorder( + options: RoundedRectDottedBorderOptions( + radius: const Radius.circular(12), + dashPattern: const [6, 6, 6, 6], + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + ), + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, borderRadius: const BorderRadius.all(Radius.circular(12))), + child: SizedBox( + height: Responsive.height(20, context), + width: Responsive.width(90, context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset('assets/icons/ic_folder.svg'), + const SizedBox(height: 10), + Text( + "Choose a image and upload here".tr, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontFamily: AppThemeData.medium, fontSize: 16), + ), + const SizedBox(height: 5), + Text("JPEG, PNG".tr, style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700, fontFamily: AppThemeData.regular)), + const SizedBox(height: 10), + RoundedButtonFill( + title: "Brows Image".tr, + color: AppThemeData.primary50, + width: 30, + height: 5, + textColor: AppThemeData.primary300, + onPress: () async { + buildBottomSheet(context, controller); + }, + ), + ], + ), + ), + ), + ), + const SizedBox(height: 10), + controller.images.isEmpty + ? const SizedBox() + : SizedBox( + height: 90, + child: Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: controller.images.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + // physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: + (controller.images[index] is XFile) + ? Image.file(File((controller.images[index] as XFile).path), fit: BoxFit.cover, width: 80, height: 80) + : NetworkImageWidget(imageUrl: controller.images[index]?.toString() ?? '', fit: BoxFit.cover, width: 80, height: 80), + // controller.images[index].runtimeType == XFile + // ? Image.file(File(controller.images[index].path), fit: BoxFit.cover, width: 80, height: 80) + // : NetworkImageWidget(imageUrl: controller.images[index], fit: BoxFit.cover, width: 80, height: 80), + ), + Positioned( + bottom: 0, + top: 0, + left: 0, + right: 0, + child: InkWell( + onTap: () { + controller.images.removeAt(index); + }, + child: const Icon(Icons.remove_circle, size: 28, color: AppThemeData.danger300), + ), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 10), + ], + ), + ), + DottedBorder( + options: RoundedRectDottedBorderOptions( + radius: const Radius.circular(12), + dashPattern: const [6, 6, 6, 6], + color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, + ), + child: TextFormField( + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.sentences, + controller: controller.commentController.value, + maxLines: 4, + textInputAction: TextInputAction.done, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium), + decoration: InputDecoration( + errorStyle: const TextStyle(color: Colors.red), + filled: true, + fillColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + disabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + border: InputBorder.none, + hintText: "Type comment".tr, + hintStyle: TextStyle(fontSize: 14, color: isDark ? AppThemeData.grey600 : AppThemeData.grey400, fontFamily: AppThemeData.regular), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Submit Review".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + controller.saveRating(); + }, + ), + ), + ), + ); + }, + ); + } + + Future buildBottomSheet(BuildContext context, RateProductController controller) { + return showModalBottomSheet( + context: context, + builder: (context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return StatefulBuilder( + builder: (context, setState) { + return SizedBox( + height: Responsive.height(22, context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 15), + child: Text("Please Select".tr, style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.bold, fontSize: 16)), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton(onPressed: () => controller.pickFile(source: ImageSource.camera), icon: const Icon(Icons.camera_alt, size: 32)), + Padding(padding: const EdgeInsets.only(top: 3), child: Text("Camera".tr)), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton(onPressed: () => controller.pickFile(source: ImageSource.gallery), icon: const Icon(Icons.photo_library_sharp, size: 32)), + Padding(padding: const EdgeInsets.only(top: 3), child: Text("Gallery".tr)), + ], + ), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/refer_friend_screen/refer_friend_screen.dart b/lib/screen_ui/multi_vendor_service/refer_friend_screen/refer_friend_screen.dart new file mode 100644 index 0000000..f2f6c03 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/refer_friend_screen/refer_friend_screen.dart @@ -0,0 +1,156 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/refer_friend_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; + +class ReferFriendScreen extends StatelessWidget { + const ReferFriendScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ReferFriendController(), + builder: (controller) { + return Scaffold( + body: + controller.isLoading.value + ? Constant.loader() + : Container( + width: Responsive.width(100, context), + height: Responsive.height(100, context), + decoration: const BoxDecoration(image: DecorationImage(image: AssetImage("assets/images/refer_friend.png"), fit: BoxFit.fill)), + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: const Icon(Icons.arrow_back, color: AppThemeData.grey50), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 60), + Center(child: SvgPicture.asset("assets/images/referal_top.svg")), + const SizedBox(height: 10), + Text( + "Refer your friend and earn".tr, + style: TextStyle(fontSize: 22, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + const SizedBox(width: 4), + Text( + "${Constant.amountShow(amount: Constant.sectionConstantModel!.referralAmount)} ${'Each🎉'.tr}", + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 32), + Text( + "Invite Friends & Businesses".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.ecommerce100 : AppThemeData.ecommerceDark100, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + "${'Invite your friends to sign up with Foodie using your code, and you’ll earn'.tr} ${Constant.amountShow(amount: Constant.sectionConstantModel!.referralAmount)} ${'after their Success the first order! 💸🍔'.tr}" + .tr, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 40), + Container( + decoration: ShapeDecoration( + gradient: const LinearGradient(begin: Alignment(0.00, -1.00), end: Alignment(0, 1), colors: [Color(0xFF271366), Color(0xFF4826B2)]), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + shadows: const [BoxShadow(color: Color(0x14FFFFFF), blurRadius: 120, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 80, vertical: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.referralModel.value.referralCode.toString(), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.ecommerce100 : AppThemeData.ecommerceDark100, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: controller.referralModel.value.referralCode.toString())); + ShowToastDialog.showToast("Copied".tr); + }, + child: const Icon(Icons.copy, color: AppThemeData.ecommerce100), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Row( + children: [ + Expanded(child: Divider(thickness: 1, color: isDark ? AppThemeData.ecommerce100 : AppThemeData.ecommerceDark100)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), + child: Text( + "or".tr, + textAlign: TextAlign.center, + style: TextStyle( + color: isDark ? AppThemeData.ecommerce100 : AppThemeData.ecommerceDark100, + fontSize: 12, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded(child: Divider(color: isDark ? AppThemeData.ecommerce100 : AppThemeData.ecommerceDark100)), + ], + ), + ), + RoundedButtonFill( + title: "Share Code".tr, + width: 55, + color: AppThemeData.ecommerce300, + textColor: AppThemeData.grey50, + onPress: () async { + await Share.share( + "${"Hey there, thanks for choosing Foodie. Hope you love our product. If you do, share it with your friends using code".tr} ${controller.referralModel.value.referralCode.toString()} ${"and get".tr}${Constant.amountShow(amount: Constant.sectionConstantModel!.referralAmount.toString())} ${"when order completed".tr}", + ); + }, + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart b/lib/screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart new file mode 100644 index 0000000..690a53e --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/restaurant_details_screen/restaurant_details_screen.dart @@ -0,0 +1,1920 @@ +import 'package:badges/badges.dart' as badges; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/restaurant_details_controller.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/models/favourite_item_model.dart'; +import 'package:customer/models/favourite_model.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_category_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../cart_screen/cart_screen.dart'; +import '../dine_in_screeen/dine_in_details_screen.dart'; +import '../review_list_screen/review_list_screen.dart'; + +class RestaurantDetailsScreen extends StatelessWidget { + const RestaurantDetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RestaurantDetailsController(), + autoRemove: false, + builder: (controller) { + return Scaffold( + bottomNavigationBar: + cartItem.isEmpty + ? null + : InkWell( + onTap: () { + Get.to(const CartScreen()); + }, + child: Container( + height: 60, + decoration: BoxDecoration(color: AppThemeData.primary300), + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${cartItem.length} ${"items".tr}', style: TextStyle(fontFamily: AppThemeData.medium, color: AppThemeData.grey50, fontSize: 16)), + Text('View Cart'.tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: AppThemeData.grey50, fontSize: 16, fontWeight: FontWeight.bold)), + ], + ), + ), + ), + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + expandedHeight: Responsive.height(30, context), + floating: true, + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Row( + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.arrow_back, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + const Expanded(child: SizedBox()), + Visibility( + visible: (controller.vendorModel.value.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.primary300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text("Free Delivery".tr, style: TextStyle(fontSize: 14, color: AppThemeData.carRent600, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600)), + ], + ), + ), + const SizedBox(width: 10), + ], + ), + ), + InkWell( + onTap: () async { + if (controller.favouriteList.where((p0) => p0.restaurantId == controller.vendorModel.value.id).isNotEmpty) { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: controller.vendorModel.value.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.removeWhere((item) => item.restaurantId == controller.vendorModel.value.id); + await FireStoreUtils.removeFavouriteRestaurant(favouriteModel); + } else { + FavouriteModel favouriteModel = FavouriteModel(restaurantId: controller.vendorModel.value.id, userId: FireStoreUtils.getCurrentUid()); + controller.favouriteList.add(favouriteModel); + await FireStoreUtils.setFavouriteRestaurant(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteList.where((p0) => p0.restaurantId == controller.vendorModel.value.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg", colorFilter: const ColorFilter.mode(AppThemeData.grey50, BlendMode.srcIn)) + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + const SizedBox(width: 10), + Obx( + () => badges.Badge( + showBadge: cartItem.isEmpty ? false : true, + badgeContent: Text( + "${cartItem.length}", + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + ), + ), + badgeStyle: badges.BadgeStyle(shape: badges.BadgeShape.circle, badgeColor: AppThemeData.ecommerce300), + child: InkWell( + onTap: () { + Get.to(const CartScreen()); + }, + child: ClipOval( + child: SvgPicture.asset("assets/icons/ic_shoping_cart.svg", width: 24, height: 24, colorFilter: const ColorFilter.mode(AppThemeData.grey50, BlendMode.srcIn)), + ), + ), + ), + ), + ], + ), + flexibleSpace: FlexibleSpaceBar( + background: Stack( + children: [ + controller.vendorModel.value.photos == null || controller.vendorModel.value.photos!.isEmpty + ? Stack( + children: [ + NetworkImageWidget( + imageUrl: controller.vendorModel.value.photo.toString(), + fit: BoxFit.cover, + width: Responsive.width(100, context), + height: Responsive.height(40, context), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), Colors.black]), + ), + ), + ], + ) + : PageView.builder( + physics: const BouncingScrollPhysics(), + controller: controller.pageController.value, + scrollDirection: Axis.horizontal, + itemCount: controller.vendorModel.value.photos!.length, + padEnds: false, + pageSnapping: true, + allowImplicitScrolling: true, + itemBuilder: (BuildContext context, int index) { + String image = controller.vendorModel.value.photos![index]; + return Stack( + children: [ + NetworkImageWidget(imageUrl: image.toString(), fit: BoxFit.cover, width: Responsive.width(100, context), height: Responsive.height(40, context)), + Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), Colors.black]), + ), + ), + ], + ); + }, + ), + Positioned( + bottom: 10, + right: 0, + left: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(controller.vendorModel.value.photos!.length, (index) { + return Obx( + () => Container( + margin: const EdgeInsets.only(right: 5), + alignment: Alignment.centerLeft, + height: 9, + width: 9, + decoration: BoxDecoration(shape: BoxShape.circle, color: controller.currentPage.value == index ? AppThemeData.primary300 : AppThemeData.grey300), + ), + ); + }), + ), + ), + ], + ), + ), + ), + ]; + }, + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.vendorModel.value.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 22, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + SizedBox( + width: Responsive.width(78, context), + child: Text( + controller.vendorModel.value.location.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey400 : AppThemeData.grey400), + ), + ), + ], + ), + ), + Column( + children: [ + Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + Constant.calculateReview( + reviewCount: controller.vendorModel.value.reviewsCount.toString(), + reviewSum: controller.vendorModel.value.reviewsSum.toString(), + ), + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + InkWell( + onTap: () { + Get.to(const ReviewListScreen(), arguments: {"vendorModel": controller.vendorModel.value}); + }, + child: Text( + "${controller.vendorModel.value.reviewsCount} ${'Ratings'.tr}", + style: TextStyle(decoration: TextDecoration.underline, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700, fontFamily: AppThemeData.regular), + ), + ), + ], + ), + ], + ), + Constant.sectionConstantModel!.serviceTypeFlag == "ecommerce-service" + ? SizedBox() + : Row( + children: [ + Text( + controller.isOpen.value ? "Open".tr : "Close".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 14, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: controller.isOpen.value ? AppThemeData.success400 : AppThemeData.danger300, + ), + ), + Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Icon(Icons.circle, size: 5, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500)), + InkWell( + onTap: () { + if (controller.vendorModel.value.workingHours!.isEmpty) { + ShowToastDialog.showToast("Timing is not added by store".tr); + } else { + timeShowBottomSheet(context, controller); + } + }, + child: Text( + "View Timings".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.underline, + decorationColor: AppThemeData.ecommerce300, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + ), + ), + ), + ], + ), + controller.vendorModel.value.dineInActive == true || (controller.vendorModel.value.openDineTime != null && controller.vendorModel.value.openDineTime!.isNotEmpty) + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Also applicable on table booking".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + Get.to(const DineInDetailsScreen(), arguments: {"vendorModel": controller.vendorModel.value}); + }, + child: Container( + height: 80, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + borderRadius: BorderRadius.circular(16), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/ic_table.gif"), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Table Booking".tr, + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + Text( + "Quick Conformations".tr, + style: TextStyle( + fontSize: 12, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ) + : const SizedBox(), + controller.couponList.isEmpty + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Additional Offers".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + CouponListView(controller: controller), + ], + ), + const SizedBox(height: 20), + Text( + "Menu".tr, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + TextFieldWidget( + controller: controller.searchEditingController.value, + hintText: 'Search the item and more...'.tr, + onchange: (value) { + controller.searchProduct(value); + }, + prefix: Padding(padding: const EdgeInsets.all(12), child: SvgPicture.asset("assets/icons/ic_search.svg")), + ), + const SizedBox(height: 10), + Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox() + : Row( + children: [ + InkWell( + onTap: () { + if (controller.isVag.value == true) { + controller.isVag.value = false; + } else { + controller.isVag.value = true; + } + controller.filterRecord(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: + controller.isVag.value + ? ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: AppThemeData.primary300), borderRadius: BorderRadius.circular(120)), + ) + : ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_veg.svg", height: 20, width: 20), + const SizedBox(width: 8), + Text( + 'Veg'.tr, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () { + if (controller.isNonVag.value == true) { + controller.isNonVag.value = false; + } else { + controller.isNonVag.value = true; + } + controller.filterRecord(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: + controller.isNonVag.value + ? ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: AppThemeData.primary300), borderRadius: BorderRadius.circular(120)), + ) + : ShapeDecoration( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_nonveg.svg", height: 20, width: 20), + const SizedBox(width: 8), + Text( + 'Non Veg'.tr, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + ProductListView(controller: controller), + ], + ), + ), + ), + ), + // floatingActionButton: PopupMenuButton( + // offset: const Offset(0, -260), + // onOpened: () { + // controller.isMenuOpen.value = true; + // }, + // onCanceled: () { + // controller.isMenuOpen.value = false; + // }, + // onSelected: (value) { + // controller.isMenuOpen.value = false; + // }, + // color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + // shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))), + // itemBuilder: (context) { + // return List.generate(controller.vendorCategoryList.length, (index) { + // VendorCategoryModel vendorCategoryModel = controller.vendorCategoryList[index]; + // return PopupMenuItem( + // value: index, + // onTap: () {}, + // child: SizedBox( + // width: 230, + // child: Text( + // vendorCategoryModel.title.toString(), + // textAlign: TextAlign.start, + // maxLines: 1, + // style: TextStyle( + // fontSize: 14, + // overflow: TextOverflow.ellipsis, + // fontFamily: AppThemeData.semiBold, + // fontWeight: FontWeight.w600, + // color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + // ), + // ), + // ), + // ); + // }); + // }, + // child: Container( + // width: 60, + // height: 60, + // padding: const EdgeInsets.all(10), + // decoration: ShapeDecoration( + // color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(120), + // ), + // ), + // child: controller.isMenuOpen.value + // ? Icon( + // Icons.close, + // color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + // ) + // : Column( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // SvgPicture.asset("assets/icons/ic_book.svg"), + // Text( + // "Menu", + // textAlign: TextAlign.start, + // maxLines: 1, + // style: TextStyle( + // fontSize: 12, + // overflow: TextOverflow.ellipsis, + // fontFamily: AppThemeData.medium, + // fontWeight: FontWeight.w500, + // color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + // ), + // ), + // ], + // ), + // ), + // ), + ); + }, + ); + } + + Future timeShowBottomSheet(BuildContext context, RestaurantDetailsController productModel) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(30))), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: + (context) => FractionallySizedBox( + heightFactor: 0.70, + child: StatefulBuilder( + builder: (context1, setState) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Container( + width: 134, + height: 5, + margin: const EdgeInsets.only(bottom: 6), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey50 : AppThemeData.grey800, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3))), + ), + ), + ), + Expanded( + child: ListView.builder( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemCount: productModel.vendorModel.value.workingHours!.length, + itemBuilder: (context, dayIndex) { + WorkingHours workingHours = productModel.vendorModel.value.workingHours![dayIndex]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${workingHours.day}", + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + const SizedBox(height: 10), + workingHours.timeslot == null || workingHours.timeslot!.isEmpty + ? const SizedBox() + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: workingHours.timeslot!.length, + itemBuilder: (context, timeIndex) { + Timeslot timeSlotModel = workingHours.timeslot![timeIndex]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: isDark ? AppThemeData.grey400 : AppThemeData.grey200), + ), + child: Center( + child: Text( + timeSlotModel.from.toString(), + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: isDark ? AppThemeData.grey400 : AppThemeData.grey200), + ), + child: Center( + child: Text( + timeSlotModel.to.toString(), + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} + +class CouponListView extends StatelessWidget { + final RestaurantDetailsController controller; + + const CouponListView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return SizedBox( + height: Responsive.height(9, context), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: controller.couponList.length, + itemBuilder: (BuildContext context, int index) { + CouponModel offerModel = controller.couponList[index]; + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), borderRadius: BorderRadius.circular(16)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: SizedBox( + width: Responsive.width(80, context), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 60, + decoration: const BoxDecoration(image: DecorationImage(image: AssetImage("assets/images/offer_gif.gif"), fit: BoxFit.fill)), + child: Center( + child: Text( + offerModel.discountType == "Fix Price" ? Constant.amountShow(amount: offerModel.discount) : "${offerModel.discount}%", + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, fontSize: 12), + ), + ), + ), + const SizedBox(width: 10), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + offerModel.description.toString(), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: offerModel.code.toString())).then((value) { + ShowToastDialog.showToast("Copied".tr); + }); + }, + child: Row( + children: [ + Text( + offerModel.code.toString(), + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + const SizedBox(width: 5), + SvgPicture.asset("assets/icons/ic_copy.svg"), + const SizedBox(height: 10, child: VerticalDivider()), + const SizedBox(width: 5), + Text( + Constant.timestampToDateTime(offerModel.expiresAt!), + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class ProductListView extends StatelessWidget { + final RestaurantDetailsController controller; + + const ProductListView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: controller.vendorCategoryList.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + VendorCategoryModel vendorCategoryModel = controller.vendorCategoryList[index]; + return ExpansionTile( + childrenPadding: EdgeInsets.zero, + tilePadding: EdgeInsets.zero, + shape: const Border(), + initiallyExpanded: true, + title: Text( + "${vendorCategoryModel.title.toString()} (${controller.productList.where((p0) => p0.categoryID == vendorCategoryModel.id).toList().length})", + style: TextStyle(fontSize: 18, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + children: [ + Obx( + () => ListView.builder( + itemCount: controller.productList.where((p0) => p0.categoryID == vendorCategoryModel.id).toList().length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + ProductModel productModel = controller.productList.where((p0) => p0.categoryID == vendorCategoryModel.id).toList()[index]; + + String price = "0.0"; + String disPrice = "0.0"; + List selectedVariants = []; + List selectedIndexVariants = []; + List selectedIndexArray = []; + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + selectedVariants.add(productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString()); + selectedIndexVariants.add('${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}'); + selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + price = Constant.productCommissionPrice( + controller.vendorModel.value, + productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0', + ); + disPrice = "0"; + } + } else { + price = Constant.productCommissionPrice(controller.vendorModel.value, productModel.price.toString()); + disPrice = double.parse(productModel.disPrice.toString()) <= 0 ? "0" : Constant.productCommissionPrice(controller.vendorModel.value, productModel.disPrice.toString()); + } + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox() + : Row( + children: [ + productModel.nonveg == true ? SvgPicture.asset("assets/icons/ic_nonveg.svg") : SvgPicture.asset("assets/icons/ic_veg.svg"), + const SizedBox(width: 5), + Text( + productModel.nonveg == true ? "Non Veg.".tr : "Pure veg.".tr, + style: TextStyle( + color: productModel.nonveg == true ? AppThemeData.danger300 : AppThemeData.success400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 5), + Text( + productModel.name.toString(), + style: TextStyle(fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + double.parse(disPrice) <= 0 + ? Text( + Constant.amountShow(amount: price), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ) + : Row( + children: [ + Text( + Constant.amountShow(amount: disPrice), + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: price), + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.lineThrough, + decorationColor: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + color: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: const ColorFilter.mode(AppThemeData.warning300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: productModel.reviewsCount!.toStringAsFixed(0), reviewSum: productModel.reviewsSum.toString())} (${productModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + ], + ), + Text( + "${productModel.description}", + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 5), + InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return infoDialog(controller, isDark, productModel); + }, + ); + }, + child: Row( + children: [ + Icon(Icons.info, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, size: 18), + const SizedBox(width: 8), + Text( + "Info".tr, + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontSize: 16, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + ), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: productModel.photo.toString(), fit: BoxFit.cover, height: Responsive.height(16, context), width: Responsive.width(34, context)), + Container( + height: Responsive.height(16, context), + width: Responsive.width(34, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () async { + if (controller.favouriteItemList.where((p0) => p0.productId == productModel.id).isNotEmpty) { + FavouriteItemModel favouriteModel = FavouriteItemModel( + productId: productModel.id, + storeId: controller.vendorModel.value.id, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteItemList.removeWhere((item) => item.productId == productModel.id); + await FireStoreUtils.removeFavouriteItem(favouriteModel); + } else { + FavouriteItemModel favouriteModel = FavouriteItemModel( + productId: productModel.id, + storeId: controller.vendorModel.value.id, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteItemList.add(favouriteModel); + + await FireStoreUtils.setFavouriteItem(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteItemList.where((p0) => p0.productId == productModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg"), + ), + ), + ), + controller.isOpen.value == false || Constant.userModel == null + ? const SizedBox() + : Positioned( + bottom: 10, + left: 20, + right: 20, + child: + selectedVariants.isNotEmpty || (productModel.addOnsTitle != null && productModel.addOnsTitle!.isNotEmpty) + ? RoundedButtonFill( + title: "Add".tr, + width: 10, + height: 4, + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + textColor: AppThemeData.primary300, + onPress: () async { + controller.selectedVariants.clear(); + controller.selectedIndexVariants.clear(); + controller.selectedIndexArray.clear(); + controller.selectedAddOns.clear(); + controller.quantity.value = 1; + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + controller.selectedVariants.add( + productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString(), + ); + controller.selectedIndexVariants.add( + '${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}', + ); + controller.selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + final bool productIsInList = cartItem.any( + (product) => + product.id == + "${productModel.id}~${productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).isNotEmpty ? productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).first.variantId.toString() : ""}", + ); + + if (productIsInList) { + CartProductModel element = cartItem.firstWhere( + (product) => + product.id == + "${productModel.id}~${productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).isNotEmpty ? productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).first.variantId.toString() : ""}", + ); + controller.quantity.value = element.quantity!; + if (element.extras != null) { + for (var element in element.extras!) { + controller.selectedAddOns.add(element); + } + } + } + } else { + if (cartItem.where((product) => product.id == "${productModel.id}").isNotEmpty) { + CartProductModel element = cartItem.firstWhere((product) => product.id == "${productModel.id}"); + controller.quantity.value = element.quantity!; + if (element.extras != null) { + for (var element in element.extras!) { + controller.selectedAddOns.add(element); + } + } + } + } + controller.update(); + controller.calculatePrice(productModel); + productDetailsBottomSheet(context, productModel); + }, + ) + : Obx( + () => + cartItem.where((p0) => p0.id == productModel.id).isNotEmpty + ? Container( + width: Responsive.width(100, context), + height: Responsive.height(4, context), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(200)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () { + controller.addToCart( + productModel: productModel, + price: price, + discountPrice: disPrice, + isIncrement: false, + quantity: cartItem.where((p0) => p0.id == productModel.id).first.quantity! - 1, + ); + }, + child: Icon(Icons.remove, color: isDark ? AppThemeData.grey50 : AppThemeData.greyDark50), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Text( + cartItem.where((p0) => p0.id == productModel.id).first.quantity.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + InkWell( + onTap: () { + if ((cartItem.where((p0) => p0.id == productModel.id).first.quantity ?? 0) < (productModel.quantity ?? 0) || + (productModel.quantity ?? 0) == -1) { + controller.addToCart( + productModel: productModel, + price: price, + discountPrice: disPrice, + isIncrement: true, + quantity: cartItem.where((p0) => p0.id == productModel.id).first.quantity! + 1, + ); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + }, + child: Icon(Icons.add, color: isDark ? AppThemeData.grey50 : AppThemeData.greyDark50), + ), + ], + ), + ) + : RoundedButtonFill( + title: "Add".tr, + width: 10, + height: 4, + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + textColor: AppThemeData.primary300, + onPress: () async { + if (1 <= (productModel.quantity ?? 0) || (productModel.quantity ?? 0) == -1) { + controller.addToCart(productModel: productModel, price: price, discountPrice: disPrice, isIncrement: true, quantity: 1); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + }, + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + }, + ), + ); + } + + Future productDetailsBottomSheet(BuildContext context, ProductModel productModel) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(30))), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: + (context) => FractionallySizedBox( + heightFactor: 0.85, + child: StatefulBuilder( + builder: (context1, setState) { + return ProductDetailsView(productModel: productModel); + }, + ), + ), + ); + } + + Dialog infoDialog(RestaurantDetailsController controller, isDark, ProductModel productModel) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + insetPadding: const EdgeInsets.all(10), + clipBehavior: Clip.antiAliasWithSaveLayer, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + child: Padding( + padding: const EdgeInsets.all(30), + child: SizedBox( + width: 500, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Product Information's".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.bold, fontWeight: FontWeight.w700, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + Text( + productModel.description.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + productModel.grams == 0 && Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Gram".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + productModel.grams.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ), + productModel.calories == 0 && Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Calories".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + productModel.calories.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ), + productModel.proteins == 0 && Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Proteins".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + productModel.proteins.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ), + productModel.fats == 0 && Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + "Fats".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + ), + Text( + productModel.fats.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ), + + productModel.productSpecification != null && productModel.productSpecification!.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(top: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Specification".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w700, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + SizedBox(height: 8), + ListView.builder( + itemCount: productModel.productSpecification!.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + productModel.productSpecification!.keys.elementAt(index), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 16), + ), + Text( + productModel.productSpecification!.values.elementAt(index), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ); + }, + ), + ], + ), + ) + : const SizedBox(), + + productModel.brandId != null && productModel.brandId!.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(top: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Brand".tr, + textAlign: TextAlign.start, + style: TextStyle(fontWeight: FontWeight.w700, fontFamily: AppThemeData.bold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + SizedBox(height: 5), + Text( + controller.getBrandName(productModel.brandId!), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16), + ), + ], + ), + ) + : const SizedBox(), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Back".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.back(); + }, + ), + ], + ), + ), + ), + ), + ); + } +} + +class ProductDetailsView extends StatelessWidget { + final ProductModel productModel; + + const ProductDetailsView({super.key, required this.productModel}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RestaurantDetailsController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget(imageUrl: productModel.photo.toString(), height: Responsive.height(11, context), width: Responsive.width(22, context), fit: BoxFit.cover), + Container( + height: Responsive.height(11, context), + width: Responsive.width(22, context), + decoration: BoxDecoration( + gradient: LinearGradient(begin: const Alignment(-0.00, -1.00), end: const Alignment(0, 1), colors: [Colors.black.withOpacity(0), const Color(0xFF111827)]), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + productModel.name.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + ), + InkWell( + onTap: () async { + if (controller.favouriteItemList.where((p0) => p0.productId == productModel.id).isNotEmpty) { + FavouriteItemModel favouriteModel = FavouriteItemModel( + productId: productModel.id, + storeId: controller.vendorModel.value.id, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteItemList.removeWhere((item) => item.productId == productModel.id); + await FireStoreUtils.removeFavouriteItem(favouriteModel); + } else { + FavouriteItemModel favouriteModel = FavouriteItemModel( + productId: productModel.id, + storeId: controller.vendorModel.value.id, + userId: FireStoreUtils.getCurrentUid(), + ); + controller.favouriteItemList.add(favouriteModel); + + await FireStoreUtils.setFavouriteItem(favouriteModel); + } + }, + child: Obx( + () => + controller.favouriteItemList.where((p0) => p0.productId == productModel.id).isNotEmpty + ? SvgPicture.asset("assets/icons/ic_like_fill.svg") + : SvgPicture.asset("assets/icons/ic_like.svg", colorFilter: const ColorFilter.mode(AppThemeData.grey500, BlendMode.srcIn)), + ), + ), + ], + ), + Text( + productModel.description.toString(), + textAlign: TextAlign.start, + style: TextStyle(fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + productModel.itemAttribute == null || productModel.itemAttribute!.attributes!.isEmpty + ? const SizedBox() + : ListView.builder( + itemCount: productModel.itemAttribute!.attributes!.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + String title = ""; + for (var element in controller.attributesList) { + if (productModel.itemAttribute!.attributes![index].attributeId == element.id) { + title = element.title.toString(); + } + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + productModel.itemAttribute!.attributes![index].attributeOptions!.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + title, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + "Required • Select any 1 option".tr, + style: TextStyle( + fontSize: 12, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey500, + ), + ), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + ], + ) + : Offstage(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: + List.generate(productModel.itemAttribute!.attributes![index].attributeOptions!.length, (i) { + return InkWell( + onTap: () async { + if (controller.selectedIndexVariants.where((element) => element.contains('$index _')).isEmpty) { + controller.selectedVariants.insert(index, productModel.itemAttribute!.attributes![index].attributeOptions![i].toString()); + controller.selectedIndexVariants.add('$index _${productModel.itemAttribute!.attributes![index].attributeOptions![i].toString()}'); + controller.selectedIndexArray.add('${index}_$i'); + } else { + controller.selectedIndexArray.remove( + '${index}_${productModel.itemAttribute!.attributes![index].attributeOptions?.indexOf(controller.selectedIndexVariants.where((element) => element.contains('$index _')).first.replaceAll('$index _', ''))}', + ); + controller.selectedVariants.removeAt(index); + controller.selectedIndexVariants.remove(controller.selectedIndexVariants.where((element) => element.contains('$index _')).first); + controller.selectedVariants.insert(index, productModel.itemAttribute!.attributes![index].attributeOptions![i].toString()); + controller.selectedIndexVariants.add('$index _${productModel.itemAttribute!.attributes![index].attributeOptions![i].toString()}'); + controller.selectedIndexArray.add('${index}_$i'); + } + + final bool productIsInList = cartItem.any( + (product) => + product.id == + "${productModel.id}~${productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).isNotEmpty ? productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).first.variantId.toString() : ""}", + ); + if (productIsInList) { + CartProductModel element = cartItem.firstWhere( + (product) => + product.id == + "${productModel.id}~${productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).isNotEmpty ? productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).first.variantId.toString() : ""}", + ); + controller.quantity.value = element.quantity!; + } else { + controller.quantity.value = 1; + } + + controller.update(); + controller.calculatePrice(productModel); + }, + child: Chip( + shape: const RoundedRectangleBorder(side: BorderSide(color: Colors.transparent), borderRadius: BorderRadius.all(Radius.circular(20))), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + productModel.itemAttribute!.attributes![index].attributeOptions![i].toString(), + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: + controller.selectedVariants.contains(productModel.itemAttribute!.attributes![index].attributeOptions![i].toString()) + ? Colors.white + : isDark + ? AppThemeData.greyDark800 + : AppThemeData.grey800, + ), + ), + ], + ), + backgroundColor: + controller.selectedVariants.contains(productModel.itemAttribute!.attributes![index].attributeOptions![i].toString()) + ? AppThemeData.primary300 + : isDark + ? AppThemeData.grey800 + : AppThemeData.grey100, + elevation: 6.0, + padding: const EdgeInsets.all(8.0), + ), + ); + }).toList(), + ), + ), + ], + ), + ), + ), + ); + }, + ), + productModel.addOnsTitle == null || productModel.addOnsTitle!.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + "Addons".tr, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + ListView.builder( + itemCount: productModel.addOnsTitle!.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + String title = productModel.addOnsTitle![index]; + String price = productModel.addOnsPrice![index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + children: [ + Expanded( + child: Text( + title, + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + Text( + Constant.amountShow(amount: Constant.productCommissionPrice(controller.vendorModel.value, price)), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + const SizedBox(width: 10), + Obx( + () => SizedBox( + height: 24.0, + width: 24.0, + child: Checkbox( + value: controller.selectedAddOns.contains(title), + activeColor: AppThemeData.primary300, + onChanged: (value) { + if (value != null) { + if (value == true) { + controller.selectedAddOns.add(title); + } else { + controller.selectedAddOns.remove(title); + } + controller.update(); + } + }, + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ), + ], + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey800 : AppThemeData.grey100, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + children: [ + Expanded( + child: Container( + width: Responsive.width(100, context), + height: Responsive.height(5.5, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(200))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () { + if (controller.quantity.value > 1) { + controller.quantity.value -= 1; + controller.update(); + } + }, + child: Icon(Icons.remove, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + controller.quantity.value.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, + ), + ), + ), + InkWell( + onTap: () { + if (productModel.itemAttribute == null) { + if (controller.quantity.value < (productModel.quantity ?? 0) || (productModel.quantity ?? 0) == -1) { + controller.quantity.value += 1; + controller.update(); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + } else { + int totalQuantity = int.parse( + productModel.itemAttribute!.variants!.where((element) => element.variantSku == controller.selectedVariants.join('-')).first.variantQuantity.toString(), + ); + if (controller.quantity.value < totalQuantity || totalQuantity == -1) { + controller.quantity.value += 1; + controller.update(); + } else { + ShowToastDialog.showToast("Out of stock".tr); + } + } + }, + child: Icon(Icons.add, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 2, + child: RoundedButtonFill( + title: "${'Add item'.tr} ${Constant.amountShow(amount: controller.calculatePrice(productModel))}".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + if (productModel.itemAttribute == null) { + await controller.addToCart( + productModel: productModel, + price: Constant.productCommissionPrice(controller.vendorModel.value, productModel.price.toString()), + discountPrice: double.parse(productModel.disPrice.toString()) <= 0 ? "0" : Constant.productCommissionPrice(controller.vendorModel.value, productModel.disPrice.toString()), + isIncrement: true, + quantity: controller.quantity.value, + ); + } else { + String variantPrice = "0"; + if (productModel.itemAttribute!.variants!.any((e) => e.variantSku == controller.selectedVariants.join('-'))) { + variantPrice = Constant.productCommissionPrice( + controller.vendorModel.value, + productModel.itemAttribute!.variants!.firstWhere((e) => e.variantSku == controller.selectedVariants.join('-')).variantPrice ?? '0', + ); + } + + Map mapData = {}; + for (var element in productModel.itemAttribute!.attributes!) { + mapData.addEntries([ + MapEntry( + controller.attributesList.firstWhere((e) => e.id == element.attributeId).title.toString(), + controller.selectedVariants[productModel.itemAttribute!.attributes!.indexOf(element)], + ), + ]); + } + + VariantInfo variantInfo = VariantInfo( + variantPrice: productModel.itemAttribute!.variants!.firstWhere((e) => e.variantSku == controller.selectedVariants.join('-')).variantPrice ?? '0', + variantSku: controller.selectedVariants.join('-'), + variantOptions: mapData, + variantImage: productModel.itemAttribute!.variants!.firstWhere((e) => e.variantSku == controller.selectedVariants.join('-')).variantImage ?? '', + variantId: productModel.itemAttribute!.variants!.firstWhere((e) => e.variantSku == controller.selectedVariants.join('-')).variantId ?? '0', + ); + + await controller.addToCart( + productModel: productModel, + price: variantPrice, + discountPrice: "0", + isIncrement: true, + variantInfo: variantInfo, + quantity: controller.quantity.value, + ); + } + controller.update(); + Get.back(); + }, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/review_list_screen/review_list_screen.dart b/lib/screen_ui/multi_vendor_service/review_list_screen/review_list_screen.dart new file mode 100644 index 0000000..84e25ae --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/review_list_screen/review_list_screen.dart @@ -0,0 +1,206 @@ +import 'package:customer/constant/collection_name.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/review_list_controller.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/rating_model.dart'; +import 'package:customer/models/review_attribute_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import '../../../controllers/theme_controller.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../service/fire_store_utils.dart'; +import '../chat_screens/full_screen_image_viewer.dart'; + +class ReviewListScreen extends StatelessWidget { + const ReviewListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ReviewListController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Reviews".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: + controller.isLoading.value + ? Constant.loader() + : controller.ratingList.isEmpty + ? Constant.showEmptyView(message: "No Review found".tr) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: ListView.builder( + itemCount: controller.ratingList.length, + itemBuilder: (context, index) { + RatingModel ratingModel = controller.ratingList[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), borderRadius: BorderRadius.circular(12)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(ratingModel.uname.toString(), style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 18, fontFamily: AppThemeData.semiBold)), + Visibility( + visible: ratingModel.productId != null, + child: FutureBuilder( + future: FireStoreUtils.fireStore.collection(CollectionName.vendorProducts).doc(ratingModel.productId?.split('~').first).get(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Text(''); + } else { + if (snapshot.hasError) { + return const Text(''); + } else if (snapshot.data == null) { + return const Text(''); + } else if (snapshot.data != null) { + ProductModel model = ProductModel.fromJson(snapshot.data!.data()!); + return Text( + '${'Rate for'.tr} - ${model.name ?? ''}', + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 14, fontFamily: AppThemeData.semiBold), + ); + } else { + return const Text(''); + } + } + }, + ), + ), + const SizedBox(height: 5), + RatingBar.builder( + ignoreGestures: true, + initialRating: ratingModel.rating ?? 0.0, + minRating: 1, + direction: Axis.horizontal, + itemCount: 5, + itemSize: 18, + itemPadding: const EdgeInsets.symmetric(horizontal: 2.0), + itemBuilder: (context, _) => const Icon(Icons.star, color: AppThemeData.warning300), + unratedColor: AppThemeData.grey400, + onRatingUpdate: (double rate) {}, + ), + const SizedBox(height: 5), + Visibility( + visible: ratingModel.comment != '' && ratingModel.comment != null, + child: Text( + ratingModel.comment.toString(), + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16, fontFamily: AppThemeData.medium), + ), + ), + const SizedBox(height: 5), + Visibility( + visible: ratingModel.reviewAttributes != null, + child: ListView.builder( + itemCount: ratingModel.reviewAttributes!.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + String key = ratingModel.reviewAttributes!.keys.elementAt(index); + dynamic value = ratingModel.reviewAttributes![key]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + FutureBuilder( + future: FireStoreUtils.fireStore.collection(CollectionName.reviewAttributes).doc(key).get(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Text(''); + } else { + if (snapshot.hasError) { + return const Text(''); + } else if (snapshot.data == null) { + return const Text(''); + } else { + ReviewAttributeModel model = ReviewAttributeModel.fromJson(snapshot.data!.data()!); + return Expanded( + child: Text( + model.title.toString(), + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontSize: 16, fontFamily: AppThemeData.semiBold), + ), + ); + } + } + }, + ), + RatingBar.builder( + ignoreGestures: true, + initialRating: value == null ? 0.0 : value ?? 0.0, + minRating: 1, + direction: Axis.horizontal, + itemCount: 5, + itemSize: 15, + unratedColor: AppThemeData.grey400, + itemPadding: const EdgeInsets.symmetric(horizontal: 2.0), + itemBuilder: (context, _) => const Icon(Icons.star, color: AppThemeData.warning300), + onRatingUpdate: (double rate) {}, + ), + ], + ), + ); + }, + ), + ), + if (ratingModel.photos?.isNotEmpty == true) + SizedBox( + height: Responsive.height(9, context), + child: ListView.builder( + itemCount: ratingModel.photos?.length, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + return InkWell( + onTap: () { + Get.to(FullScreenImageViewer(imageUrl: ratingModel.photos?[index])); + }, + child: Padding( + padding: const EdgeInsets.all(6.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget( + imageUrl: ratingModel.photos?[index], + height: Responsive.height(9, context), + width: Responsive.height(8, context), + fit: BoxFit.fill, + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 5), + Text( + Constant.timestampToDateTime(ratingModel.createdAt!), + style: TextStyle(color: isDark ? AppThemeData.grey300 : AppThemeData.grey600, fontSize: 14, fontFamily: AppThemeData.medium), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/scan_qrcode_screen/scan_qr_code_screen.dart b/lib/screen_ui/multi_vendor_service/scan_qrcode_screen/scan_qr_code_screen.dart new file mode 100644 index 0000000..8a8b9c4 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/scan_qrcode_screen/scan_qr_code_screen.dart @@ -0,0 +1,56 @@ +import 'package:customer/controllers/scan_qr_code_controller.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:qr_code_dart_scan/qr_code_dart_scan.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class ScanQrCodeScreen extends StatelessWidget { + const ScanQrCodeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetBuilder( + init: ScanQrCodeController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + title: Text("Scan QR Code".tr, style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500)), + ), + body: QRCodeDartScanView( + // enable scan invert qr code ( default = false) + typeScan: TypeScan.live, + // if TypeScan.takePicture will try decode when click to take a picture(default TypeScan.live) + onCapture: (Result result) { + Get.back(); + ShowToastDialog.showLoader("Please wait...".tr); + if (controller.allNearestRestaurant.isNotEmpty) { + if (controller.allNearestRestaurant.where((vendor) => vendor.id == result.text).isEmpty) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Store is not available".tr); + return; + } + VendorModel storeModel = controller.allNearestRestaurant.firstWhere((vendor) => vendor.id == result.text); + ShowToastDialog.closeLoader(); + Get.back(); + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": storeModel}); + } else { + Get.back(); + ShowToastDialog.showToast("Store is not available".tr); + } + }, + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/search_screen/search_screen.dart b/lib/screen_ui/multi_vendor_service/search_screen/search_screen.dart new file mode 100644 index 0000000..b5f0b51 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/search_screen/search_screen.dart @@ -0,0 +1,447 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/search_controller.dart'; +import 'package:customer/models/product_model.dart'; +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import '../../../controllers/theme_controller.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../../service/fire_store_utils.dart'; +import '../../../widget/restaurant_image_view.dart'; +import '../restaurant_details_screen/restaurant_details_screen.dart'; + +class SearchScreen extends StatelessWidget { + const SearchScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: SearchScreenController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text( + Constant.sectionConstantModel?.name?.toLowerCase().contains('restaurants') == true ? "Find your favorite products and nearby stores" : "Search Item & Store".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(55), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFieldWidget( + hintText: Constant.sectionConstantModel?.name?.toLowerCase().contains('restaurants') == true ? 'Find your favorite products and nearby stores'.tr : 'Search the store and item'.tr, + prefix: Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: SvgPicture.asset("assets/icons/ic_search.svg")), + controller: null, + onchange: (value) { + controller.onSearchTextChanged(value); + }, + ), + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + controller.vendorSearchList.isEmpty + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Store".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + ], + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.vendorSearchList.length, + itemBuilder: (context, index) { + VendorModel vendorModel = controller.vendorSearchList[index]; + return InkWell( + onTap: () { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": vendorModel}); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), + child: Stack( + children: [ + RestaurantImageView(vendorModel: vendorModel), + Container( + height: Responsive.height(20, context), + width: Responsive.width(100, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: Offset(Responsive.width(-3, context), Responsive.height(17.5, context)), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: (vendorModel.isSelfDelivery == true && Constant.isSelfDeliveryFeature == true), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppThemeData.success300, + borderRadius: BorderRadius.circular(120), // Optional + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_free_delivery.svg"), + const SizedBox(width: 5), + Text( + "Free Delivery".tr, + style: TextStyle(fontSize: 14, color: AppThemeData.success300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + ], + ), + ), + const SizedBox(width: 6), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.primary600 : AppThemeData.primary50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: ColorFilter.mode(AppThemeData.primary300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: vendorModel.reviewsCount!.toStringAsFixed(0), reviewSum: vendorModel.reviewsSum.toString())} (${vendorModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: ShapeDecoration( + color: isDark ? AppThemeData.ecommerce600 : AppThemeData.ecommerce50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(120)), + ), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_map_distance.svg", colorFilter: ColorFilter.mode(AppThemeData.ecommerce300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.getDistance(lat1: vendorModel.latitude.toString(), lng1: vendorModel.longitude.toString(), lat2: Constant.selectedLocation.location!.latitude.toString(), lng2: Constant.selectedLocation.location!.longitude.toString())} ${Constant.distanceType}", + style: TextStyle( + fontSize: 14, + color: isDark ? AppThemeData.ecommerce300 : AppThemeData.ecommerce300, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vendorModel.title.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + fontSize: 18, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.semiBold, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + ), + ), + Text( + vendorModel.location.toString(), + textAlign: TextAlign.start, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.medium, + fontWeight: FontWeight.w500, + color: isDark ? AppThemeData.grey400 : AppThemeData.grey400, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ); + }, + ), + controller.productSearchList.isEmpty + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Items".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + ], + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.productSearchList.length, + itemBuilder: (context, index) { + ProductModel productModel = controller.productSearchList[index]; + return FutureBuilder( + future: getPrice(productModel), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Constant.loader(); + } else { + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (snapshot.data == null) { + return const SizedBox(); + } else { + Map map = snapshot.data!; + String price = map['price']; + String disPrice = map['disPrice']; + return InkWell( + onTap: () async { + await FireStoreUtils.getVendorById(productModel.vendorID.toString()).then((value) { + if (value != null) { + Get.to(const RestaurantDetailsScreen(), arguments: {"vendorModel": value}); + } + }); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.sectionConstantModel!.isProductDetails == false + ? SizedBox() + : productModel.nonveg == true || productModel.veg == true + ? Row( + children: [ + productModel.nonveg == true ? SvgPicture.asset("assets/icons/ic_nonveg.svg") : SvgPicture.asset("assets/icons/ic_veg.svg"), + const SizedBox(width: 5), + Text( + productModel.nonveg == true ? "Non Veg.".tr : "Pure veg.".tr, + style: TextStyle( + color: productModel.nonveg == true ? AppThemeData.danger300 : AppThemeData.success400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ) + : SizedBox(), + const SizedBox(height: 5), + Text( + productModel.name.toString(), + style: TextStyle( + fontSize: 18, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + double.parse(disPrice) <= 0 + ? Text( + Constant.amountShow(amount: price), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ) + : Row( + children: [ + Text( + Constant.amountShow(amount: disPrice), + style: TextStyle( + fontSize: 16, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 5), + Text( + Constant.amountShow(amount: price), + style: TextStyle( + fontSize: 14, + decoration: TextDecoration.lineThrough, + decorationColor: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + color: isDark ? AppThemeData.grey500 : AppThemeData.grey400, + fontFamily: AppThemeData.semiBold, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Row( + children: [ + SvgPicture.asset("assets/icons/ic_star.svg", colorFilter: const ColorFilter.mode(AppThemeData.warning300, BlendMode.srcIn)), + const SizedBox(width: 5), + Text( + "${Constant.calculateReview(reviewCount: productModel.reviewsCount!.toStringAsFixed(0), reviewSum: productModel.reviewsSum.toString())} (${productModel.reviewsCount!.toStringAsFixed(0)})", + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + ], + ), + Text( + "${productModel.description}", + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, + fontFamily: AppThemeData.regular, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: Stack( + children: [ + NetworkImageWidget( + imageUrl: productModel.photo.toString(), + fit: BoxFit.cover, + height: Responsive.height(16, context), + width: Responsive.width(34, context), + ), + Container( + height: Responsive.height(16, context), + width: Responsive.width(34, context), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: const Alignment(-0.00, -1.00), + end: const Alignment(0, 1), + colors: [Colors.black.withOpacity(0), const Color(0xFF111827)], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + } + }, + ); + }, + ), + ], + ), + ), + ), + ); + }, + ); + } + + Future> getPrice(ProductModel productModel) async { + String price = "0.0"; + String disPrice = "0.0"; + List selectedVariants = []; + List selectedIndexVariants = []; + List selectedIndexArray = []; + + print("=======>"); + print(productModel.price); + print(productModel.disPrice); + + VendorModel? vendorModel = await FireStoreUtils.getVendorById(productModel.vendorID.toString()); + if (productModel.itemAttribute != null) { + if (productModel.itemAttribute!.attributes!.isNotEmpty) { + for (var element in productModel.itemAttribute!.attributes!) { + if (element.attributeOptions!.isNotEmpty) { + selectedVariants.add(productModel.itemAttribute!.attributes![productModel.itemAttribute!.attributes!.indexOf(element)].attributeOptions![0].toString()); + selectedIndexVariants.add('${productModel.itemAttribute!.attributes!.indexOf(element)} _${productModel.itemAttribute!.attributes![0].attributeOptions![0].toString()}'); + selectedIndexArray.add('${productModel.itemAttribute!.attributes!.indexOf(element)}_0'); + } + } + } + if (productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).isNotEmpty) { + price = Constant.productCommissionPrice(vendorModel!, productModel.itemAttribute!.variants!.where((element) => element.variantSku == selectedVariants.join('-')).first.variantPrice ?? '0'); + disPrice = Constant.productCommissionPrice(vendorModel, '0'); + } + } else { + price = Constant.productCommissionPrice(vendorModel!, productModel.price.toString()); + disPrice = Constant.productCommissionPrice(vendorModel, productModel.disPrice.toString()); + } + + return {'price': price, 'disPrice': disPrice}; + } +} diff --git a/lib/screen_ui/multi_vendor_service/terms_and_condition/terms_and_condition_screen.dart b/lib/screen_ui/multi_vendor_service/terms_and_condition/terms_and_condition_screen.dart new file mode 100644 index 0000000..72c8525 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/terms_and_condition/terms_and_condition_screen.dart @@ -0,0 +1,43 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_html/flutter_html.dart'; +import '../../../controllers/theme_controller.dart'; + +class TermsAndConditionScreen extends StatelessWidget { + final String? type; + + const TermsAndConditionScreen({super.key, this.type}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + backgroundColor: isDark ? AppThemeData.grey50 : AppThemeData.grey50, + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + centerTitle: false, + automaticallyImplyLeading: false, + titleSpacing: 0, + leading: InkWell( + onTap: () { + Get.back(); + }, + child: Icon(Icons.chevron_left_outlined, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + title: Text( + type == "privacy" ? "Privacy Policy".tr : "Terms & Conditions".tr, + style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontFamily: AppThemeData.bold, fontSize: 18), + ), + elevation: 0, + bottom: PreferredSize(preferredSize: const Size.fromHeight(4.0), child: Container(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200, height: 4.0)), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: SingleChildScrollView(child: Html(shrinkWrap: true, data: type == "privacy" ? Constant.privacyPolicy : Constant.termsAndConditions)), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/wallet_screen/payment_list_screen.dart b/lib/screen_ui/multi_vendor_service/wallet_screen/payment_list_screen.dart new file mode 100644 index 0000000..94b47cd --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/wallet_screen/payment_list_screen.dart @@ -0,0 +1,182 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/wallet_controller.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/text_field_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../themes/show_toast_dialog.dart'; + +class PaymentListScreen extends StatelessWidget { + const PaymentListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: WalletController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Top up Wallet".tr, style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500)), + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFieldWidget( + title: 'Amount'.tr, + hintText: 'Enter Amount'.tr, + controller: controller.topUpAmountController.value, + textInputType: const TextInputType.numberWithOptions(decimal: true, signed: true), + prefix: Padding(padding: const EdgeInsets.all(12.0), child: Text(Constant.currencyModel!.symbol.toString(), style: const TextStyle(fontSize: 20, color: AppThemeData.grey800))), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9]'))], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + "Select Top up Options".tr, + style: TextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration(borderRadius: const BorderRadius.all(Radius.circular(20)), color: isDark ? AppThemeData.grey900 : AppThemeData.grey50), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility(visible: controller.orangeMoneyModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png")), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + ], + ), + ), + bottomNavigationBar: Container( + color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: RoundedButtonFill( + title: "Top-up".tr, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + fontSizes: 16, + onPress: () async { + if (controller.topUpAmountController.value.text.isEmpty) { + ShowToastDialog.showToast("Please Enter Amount".tr); + } else { + if (double.parse(controller.topUpAmountController.value.text) >= double.parse(Constant.minimumAmountToDeposit.toString())) { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.topUpAmountController.value.text, context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.topUpAmountController.value.text); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.topUpAmountController.value.text), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.topUpAmountController.value.text, orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + } else { + ShowToastDialog.showToast("${'Please Enter minimum amount of'.tr} ${Constant.amountShow(amount: Constant.minimumAmountToDeposit)}"); + } + } + }, + ), + ), + ), + ); + }, + ); + } + + Obx cardDecoration(WalletController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart b/lib/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart new file mode 100644 index 0000000..be02e11 --- /dev/null +++ b/lib/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart @@ -0,0 +1,269 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/wallet_controller.dart'; +import 'package:customer/models/wallet_transaction_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/payment_list_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import '../../../constant/collection_name.dart'; +import '../../../controllers/theme_controller.dart'; +import '../../../models/cab_order_model.dart'; +import '../../../models/onprovider_order_model.dart'; +import '../../../models/order_model.dart'; +import '../../../models/parcel_order_model.dart'; +import '../../../models/rental_order_model.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; +import '../../../widget/my_separator.dart'; +import '../../auth_screens/login_screen.dart'; +import '../../cab_service_screens/cab_order_details.dart'; +import '../../on_demand_service/on_demand_order_details_screen.dart'; +import '../../parcel_service/parcel_order_details.dart'; +import '../../rental_service/rental_order_details_screen.dart'; +import '../order_list_screen/order_details_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +class WalletScreen extends StatelessWidget { + const WalletScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: WalletController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + body: + controller.isLoading.value + ? Constant.loader() + : Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/login.gif", height: 120), + const SizedBox(height: 12), + Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top), + child: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "My Wallet".tr, + style: TextStyle(fontSize: 24, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w500), + ), + Text( + "Keep track of your balance, transactions, and payment methods all in one place.".tr, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey900, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20)), + image: DecorationImage(image: AssetImage("assets/images/wallet.png"), fit: BoxFit.fill), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Column( + children: [ + Text( + "My Wallet".tr, + maxLines: 1, + style: TextStyle( + color: isDark ? AppThemeData.primary100 : AppThemeData.primary100, + fontSize: 16, + overflow: TextOverflow.ellipsis, + fontFamily: AppThemeData.regular, + ), + ), + Text( + Constant.amountShow(amount: controller.userModel.value.walletAmount.toString()), + maxLines: 1, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey50, fontSize: 40, overflow: TextOverflow.ellipsis, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 80), + child: RoundedButtonFill( + title: "Top up".tr, + color: AppThemeData.warning300, + textColor: AppThemeData.grey900, + onPress: () { + Get.to(const PaymentListScreen()); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ), + Expanded( + child: + controller.walletTransactionList.isEmpty + ? Constant.showEmptyView(message: "Transaction not found".tr) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: controller.walletTransactionList.length, + itemBuilder: (context, index) { + WalletTransactionModel walletTractionModel = controller.walletTransactionList[index]; + return transactionCard(controller, isDark, walletTractionModel); + }, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Column transactionCard(WalletController controller, isDark, WalletTransactionModel transactionModel) { + return Column( + children: [ + InkWell( + onTap: () async { + final orderId = transactionModel.orderId.toString(); + final orderData = await FireStoreUtils.getOrderByIdFromAllCollections(orderId); + + if (orderData != null) { + final collection = orderData['collection_name']; + + switch (collection) { + case CollectionName.parcelOrders: + Get.to(const ParcelOrderDetails(), arguments: ParcelOrderModel.fromJson(orderData)); + break; + case CollectionName.providerOrders: + Get.to(const OnDemandOrderDetailsScreen(), arguments: OnProviderOrderModel.fromJson(orderData)); + break; + case CollectionName.rentalOrders: + Get.to(() => RentalOrderDetailsScreen(), arguments: RentalOrderModel.fromJson(orderData)); + break; + case CollectionName.rides: + Get.to(const CabOrderDetails(), arguments: {"cabOrderModel": CabOrderModel.fromJson(orderData)}); + break; + case CollectionName.vendorOrders: + Get.to(const OrderDetailsScreen(), arguments: {"orderModel": OrderModel.fromJson(orderData)}); + break; + default: + ShowToastDialog.showToast("Order details not available".tr); + } + } + }, + // onTap: () async { + // await FireStoreUtils + // .getOrderByOrderId(transactionModel.orderId.toString()) + // .then((value) { + // if (value != null) { + // Get.to( + // const OrderDetailsScreen(), + // arguments: {"orderModel": value}, + // ); + // } + // }); + // }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder(side: BorderSide(width: 1, color: isDark ? AppThemeData.grey800 : AppThemeData.grey100), borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: + transactionModel.isTopup == false + ? SvgPicture.asset("assets/icons/ic_debit.svg", height: 16, width: 16) + : SvgPicture.asset("assets/icons/ic_credit.svg", height: 16, width: 16), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + transactionModel.note.toString(), + style: TextStyle(fontSize: 16, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ), + Text( + Constant.amountShow(amount: transactionModel.amount.toString()), + style: TextStyle(fontSize: 16, fontFamily: AppThemeData.medium, color: transactionModel.isTopup == true ? AppThemeData.success400 : AppThemeData.danger300), + ), + ], + ), + const SizedBox(height: 2), + Text( + Constant.timestampToDateTime(transactionModel.date!), + style: TextStyle(fontSize: 12, fontFamily: AppThemeData.medium, fontWeight: FontWeight.w500, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700), + ), + ], + ), + ), + ], + ), + ), + ), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200)), + ], + ); + } +} + +enum PaymentGateway { payFast, mercadoPago, paypal, stripe, flutterWave, payStack, razorpay, cod, wallet, midTrans, orangeMoney, xendit } diff --git a/lib/screen_ui/on_boarding_screen/on_boarding_screen.dart b/lib/screen_ui/on_boarding_screen/on_boarding_screen.dart new file mode 100644 index 0000000..efaeca3 --- /dev/null +++ b/lib/screen_ui/on_boarding_screen/on_boarding_screen.dart @@ -0,0 +1,104 @@ +import 'package:customer/constant/assets.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/on_boarding_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../utils/network_image_widget.dart'; +import '../../utils/preferences.dart'; +import '../auth_screens/login_screen.dart'; + +class OnboardingScreen extends StatelessWidget { + const OnboardingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetX( + init: OnboardingController(), + builder: (controller) { + final pageCount = controller.onboardingList.length; + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Image.asset(AppAssets.onBoardingBG, fit: BoxFit.cover), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), + child: Column( + children: [ + const SizedBox(height: 20), + Align( + alignment: Alignment.topRight, + child: RichText( + text: TextSpan( + style: AppThemeData.regularTextStyle(fontSize: 14), + children: [ + TextSpan(text: "${controller.currentPage.value + 1}", style: AppThemeData.regularTextStyle(color: AppThemeData.grey800)), + TextSpan(text: "/$pageCount", style: AppThemeData.regularTextStyle(color: AppThemeData.grey400)), + ], + ), + ), + ), + const SizedBox(height: 20), + Expanded( + child: PageView.builder( + controller: controller.pageController, + onPageChanged: controller.onPageChanged, + itemCount: pageCount, + itemBuilder: (context, index) { + final item = controller.onboardingList[index]; + return SingleChildScrollView( + child: Column( + children: [ + Text(item.title ?? '', style: AppThemeData.boldTextStyle(color: AppThemeData.grey900), textAlign: TextAlign.center), + const SizedBox(height: 5), + Text(item.description ?? '', style: AppThemeData.boldTextStyle(color: AppThemeData.grey500, fontSize: 14), textAlign: TextAlign.center), + const SizedBox(height: 40), + NetworkImageWidget(imageUrl: item.image ?? '', width: double.infinity, height: 500), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 20), + controller.currentPage.value == pageCount - 1 + ? RoundedButtonFill( + title: "Let’s Get Started".tr, + onPress: () { + _finish(); + }, + color: AppThemeData.grey900, + ) + : Row( + children: [ + Expanded(child: RoundedButtonFill(title: "Skip".tr, onPress: () => _finish(), color: AppThemeData.grey50, textColor: AppThemeData.grey900)), + const SizedBox(width: 20), + Expanded( + child: RoundedButtonFill( + title: "Next".tr, + onPress: () { + controller.nextPage(); + }, + color: AppThemeData.grey900, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + Future _finish() async { + await Preferences.setBoolean(Preferences.isFinishOnBoardingKey, true); + Get.offAll(() => const LoginScreen()); + } +} diff --git a/lib/screen_ui/on_demand_service/favourite_ondemand_screen.dart b/lib/screen_ui/on_demand_service/favourite_ondemand_screen.dart new file mode 100644 index 0000000..5764a23 --- /dev/null +++ b/lib/screen_ui/on_demand_service/favourite_ondemand_screen.dart @@ -0,0 +1,312 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/favourite_ondemmand_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/category_model.dart'; +import 'package:customer/models/provider_serivce_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/screen_ui/on_demand_service/on_demand_details_screen.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FavouriteOndemandScreen extends StatelessWidget { + const FavouriteOndemandScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: FavouriteOndemmandController(), + builder: (controller) { + return Scaffold( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.onDemand300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + const SizedBox(width: 10), + Text("Favourite Services".tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey900 : AppThemeData.grey900, fontSize: 20)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/login.gif", height: 120), + const SizedBox(height: 12), + Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: + controller.lstFav.isEmpty + ? Constant.showEmptyView(message: "Favourite Service not found.".tr) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + scrollDirection: Axis.vertical, + physics: const BouncingScrollPhysics(), + itemCount: controller.lstFav.length, + itemBuilder: (context, index) { + return FutureBuilder>( + future: FireStoreUtils.getCurrentProviderService(controller.lstFav[index]), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (!snapshot.hasData || snapshot.data == null || snapshot.data!.isEmpty) { + return const SizedBox(); // or a placeholder widget + } + + final provider = snapshot.data!.first; // safer way than [0] + + return GestureDetector( + onTap: () { + Get.to(() => OnDemandDetailsScreen(), arguments: {'providerModel': provider}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + height: MediaQuery.of(context).size.height * 0.16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1), + color: isDark ? AppThemeData.grey900 : Colors.white, + ), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), topLeft: Radius.circular(10)), + child: CachedNetworkImage( + imageUrl: provider.photos.isNotEmpty ? provider.photos.first : Constant.placeHolderImage, + height: MediaQuery.of(context).size.height * 0.16, + width: 110, + fit: BoxFit.cover, + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + provider.title ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), + ), + ), + Obx( + () => GestureDetector( + onTap: () => controller.toggleFavourite(provider), + child: Icon( + controller.lstFav.where((element) => element.service_id == provider.id).isNotEmpty ? Icons.favorite : Icons.favorite_border, + size: 24, + color: + controller.lstFav.where((element) => element.service_id == provider.id).isNotEmpty + ? AppThemeData.primary300 + : (isDark ? Colors.white38 : Colors.black38), + ), + ), + ), + ], + ), + FutureBuilder( + future: controller.getCategory(provider.categoryId ?? ""), + builder: (ctx, snap) { + if (!snap.hasData) return const SizedBox(); + return Text(snap.data?.title ?? "", style: TextStyle(fontSize: 14, color: isDark ? Colors.white : Colors.black)); + }, + ), + _buildPrice(provider, isDark: isDark), + _buildRating(provider), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + FutureBuilder>( + future: FireStoreUtils.getCurrentProviderService(controller.lstFav[index]), + builder: (context, snapshot) { + return snapshot.data != null + ? GestureDetector( + onTap: () { + Get.to(() => OnDemandDetailsScreen(), arguments: {'providerModel': snapshot.data![0]}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Container( + height: MediaQuery.of(context).size.height * 0.16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1), + color: isDark ? AppThemeData.grey900 : Colors.white, + ), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), topLeft: Radius.circular(10)), + child: CachedNetworkImage( + imageUrl: snapshot.data![0].photos.isNotEmpty ? snapshot.data![0].photos[0] : Constant.placeHolderImage, + height: MediaQuery.of(context).size.height * 0.16, + width: 110, + fit: BoxFit.cover, + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + snapshot.data![0].title ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), + ), + ), + Obx( + () => GestureDetector( + onTap: () => controller.toggleFavourite(snapshot.data![0]), + child: Icon( + controller.lstFav.where((element) => element.service_id == snapshot.data![0].id).isNotEmpty + ? Icons.favorite + : Icons.favorite_border, + size: 24, + color: + controller.lstFav.where((element) => element.service_id == snapshot.data![0].id).isNotEmpty + ? AppThemeData.primary300 + : (isDark ? Colors.white38 : Colors.black38), + ), + ), + ), + ], + ), + FutureBuilder( + future: controller.getCategory(snapshot.data![0].categoryId ?? ""), + builder: (ctx, snap) { + if (!snap.hasData) return const SizedBox(); + return Text(snap.data?.title ?? "", style: TextStyle(fontSize: 14, color: isDark ? Colors.white : Colors.black)); + }, + ), + _buildPrice(snapshot.data![0], isDark: isDark), + _buildRating(snapshot.data![0]), + ], + ), + ), + ), + ], + ), + ), + ), + ) + : Container(); + }, + ); + }, + ), + ), + ); + }, + ); + } + + Widget _buildPrice(ProviderServiceModel provider, {bool isDark = false}) { + if (provider.disPrice == "" || provider.disPrice == "0") { + return Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/${'hr'.tr}', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300), + ); + } else { + return Row( + children: [ + Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.disPrice ?? '0') : '${Constant.amountShow(amount: provider.disPrice)}/${'hr'.tr}', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300), + ), + const SizedBox(width: 5), + Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/${'hr'.tr}', + style: const TextStyle(fontSize: 12, color: Colors.grey, decoration: TextDecoration.lineThrough), + ), + ], + ); + } + } + + Widget _buildRating(ProviderServiceModel provider) { + double rating = 0; + if (provider.reviewsCount != null && provider.reviewsCount != 0) { + rating = (provider.reviewsSum ?? 0) / (provider.reviewsCount ?? 1); + } + return Container( + decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 16, color: Colors.white), + const SizedBox(width: 3), + Text(rating.toStringAsFixed(1), style: const TextStyle(letterSpacing: 0.5, fontSize: 12, color: Colors.white)), + ], + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/my_booking_on_demand_screen.dart b/lib/screen_ui/on_demand_service/my_booking_on_demand_screen.dart new file mode 100644 index 0000000..c52ab10 --- /dev/null +++ b/lib/screen_ui/on_demand_service/my_booking_on_demand_screen.dart @@ -0,0 +1,203 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../../constant/constant.dart'; +import '../../controllers/my_booking_on_demand_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/onprovider_order_model.dart'; +import '../../models/worker_model.dart'; +import '../../themes/app_them_data.dart'; +import 'on_demand_order_details_screen.dart'; + +class MyBookingOnDemandScreen extends StatelessWidget { + const MyBookingOnDemandScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: MyBookingOnDemandController(), + builder: (controller) { + return DefaultTabController( + length: controller.tabTitles.length, + initialIndex: controller.tabTitles.indexOf(controller.selectedTab.value), + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + centerTitle: false, + title: Padding(padding: const EdgeInsets.only(bottom: 10), child: Text("Booking History".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900))), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(48), + child: TabBar( + onTap: (index) { + controller.selectTab(controller.tabTitles[index]); + }, + indicatorColor: AppThemeData.grey900, + labelColor: AppThemeData.grey900, + unselectedLabelColor: AppThemeData.grey900, + labelStyle: AppThemeData.boldTextStyle(fontSize: 16), + unselectedLabelStyle: AppThemeData.mediumTextStyle(fontSize: 16), + tabs: controller.tabTitles.map((title) => Tab(child: Center(child: Text(title)))).toList(), + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : TabBarView( + children: + controller.tabTitles.map((title) { + final orders = controller.getOrdersForTab(title); + + if (orders.isEmpty) { + return Center(child: Text("No ride found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: orders.length, + itemBuilder: (context, index) { + OnProviderOrderModel onProviderOrder = orders[index]; + WorkerModel? worker = controller.getWorker(onProviderOrder.workerId); + + return InkWell( + onTap: () { + Get.to(() => OnDemandOrderDetailsScreen(), arguments: onProviderOrder); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1), + color: isDark ? AppThemeData.grey500 : Colors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: CachedNetworkImage( + imageUrl: onProviderOrder.provider.photos.first, + height: 80, + width: 80, + imageBuilder: + (context, imageProvider) => + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: + (context, url, error) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network(Constant.placeHolderImage, fit: BoxFit.cover, cacheHeight: 80, cacheWidth: 80), + ), + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + decoration: BoxDecoration(color: AppThemeData.info50, border: Border.all(color: AppThemeData.info300), borderRadius: BorderRadius.circular(12)), + child: Text(onProviderOrder.status, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.info500)), + ), + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + onProviderOrder.provider.title.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Padding(padding: const EdgeInsets.only(top: 6), child: buildPriceText(onProviderOrder)), + const SizedBox(height: 6), + if (onProviderOrder.status != Constant.orderCompleted && + onProviderOrder.status != Constant.orderCancelled && + onProviderOrder.otp != null && + onProviderOrder.otp!.isNotEmpty) + Text( + "${'OTP :'.tr} ${onProviderOrder.otp}", + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ), + ], + ), + + /// Bottom Details (Date, Provider, Worker) + buildBottomDetails(context, onProviderOrder, isDark, worker), + ], + ), + ), + ); + }, + ); + }).toList(), + ), + ), + ); + }, + ); + } + + Widget buildPriceText(OnProviderOrderModel order) { + final hasDiscount = order.provider.disPrice != "" && order.provider.disPrice != "0"; + final price = hasDiscount ? order.provider.disPrice.toString() : order.provider.price.toString(); + + return Text( + order.provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: price) : "${Constant.amountShow(amount: price)}/${'hr'.tr}", + style: AppThemeData.mediumTextStyle(fontSize: 16, color: AppThemeData.primary300), + ); + } + + Widget buildBottomDetails(BuildContext context, OnProviderOrderModel order, bool isDark, WorkerModel? worker) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.grey400 : AppThemeData.grey100, width: 1), + color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + children: [ + detailRow("Date & Time", DateFormat('dd-MMM-yyyy hh:mm a').format(order.scheduleDateTime!.toDate()), isDark), + const Divider(thickness: 1), + detailRow("Provider", order.provider.authorName.toString(), isDark), + + if (order.provider.priceUnit == "Hourly") ...[ + if (order.startTime != null) ...[const Divider(thickness: 1), detailRow("Start Time", DateFormat('dd-MMM-yyyy hh:mm a').format(order.startTime!.toDate()), isDark)], + if (order.endTime != null) ...[const Divider(thickness: 1), detailRow("End Time", DateFormat('dd-MMM-yyyy hh:mm a').format(order.endTime!.toDate()), isDark)], + ], + + if (worker != null) ...[const Divider(thickness: 1), detailRow("Worker", worker.fullName().toString(), isDark)], + ], + ), + ), + ); + } + + Widget detailRow(String label, String value, bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label.tr, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(value.tr, style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_booking_screen.dart b/lib/screen_ui/on_demand_service/on_demand_booking_screen.dart new file mode 100644 index 0000000..5ca8936 --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_booking_screen.dart @@ -0,0 +1,557 @@ +import 'package:bottom_picker/bottom_picker.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/theme_controller.dart'; +import '../../controllers/on_demand_booking_controller.dart'; +import '../../models/tax_model.dart'; +import '../../models/user_model.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../themes/text_field_widget.dart'; +import '../../widget/osm_map/map_picker_page.dart'; +import '../../widget/place_picker/location_picker_screen.dart'; +import '../../widget/place_picker/selected_location_model.dart'; +import '../location_enable_screens/address_list_screen.dart'; + +class OnDemandBookingScreen extends StatelessWidget { + const OnDemandBookingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: OnDemandBookingController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Book Service".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Services Section + Text("Services".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(controller.provider.value?.title ?? '', style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 5), + Text(controller.categoryTitle.value, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + if (controller.provider.value?.priceUnit == "Fixed") ...[ + const SizedBox(height: 20), + Row( + children: [ + GestureDetector(onTap: controller.decrementQuantity, child: Icon(Icons.remove_circle_outline, color: AppThemeData.primary300, size: 30)), + const SizedBox(width: 10), + Text('${controller.quantity.value}', style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(width: 10), + GestureDetector(onTap: controller.incrementQuantity, child: Icon(Icons.add_circle_outline, color: AppThemeData.primary300, size: 30)), + ], + ), + ], + ], + ), + ), + SizedBox(width: 10), + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.grey.shade300, + image: controller.provider.value!.photos.isNotEmpty ? DecorationImage(image: NetworkImage(controller.provider.value?.photos.first), fit: BoxFit.cover) : null, + ), + ), + ], + ), + ), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Address".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + SizedBox(height: 5), + InkWell( + onTap: () async { + if (Constant.userModel != null) { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + if (Constant.checkZoneCheck(shippingAddress.location!.latitude ?? 0.0, shippingAddress.location!.longitude ?? 0.0)) { + controller.selectedAddress.value = shippingAddress; + controller.calculatePrice(); + } else { + ShowToastDialog.showToast("Service not available in this area".tr); + } + } + }); + } else { + Constant.checkPermission( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + ShippingAddress shippingAddress = ShippingAddress(); + + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(); + ShowToastDialog.closeLoader(); + + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + shippingAddress.addressAs = "Home"; + shippingAddress.locality = address.toString(); + shippingAddress.location = UserLocation(latitude: lat, longitude: lng); + + controller.selectedAddress.value = shippingAddress; + Get.back(); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + shippingAddress.locality = "Picked from Map"; + + controller.selectedAddress.value = shippingAddress; + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + shippingAddress.locality = currentLocation; + }); + + controller.selectedAddress.value = shippingAddress; + ShowToastDialog.closeLoader(); + } + }, + context: context, + ); + } + }, + child: Text( + controller.selectedAddress.value.getFullAddress(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 15), + TextFieldWidget(title: "Description".tr, hintText: "Enter Description".tr, controller: controller.descriptionController.value, maxLine: 5), + const SizedBox(height: 10), + GestureDetector( + onTap: () { + BottomPicker.dateTime( + onSubmit: (date) { + controller.setDateTime(date); + }, + minDateTime: DateTime.now(), + buttonAlignment: MainAxisAlignment.center, + displaySubmitButton: true, + buttonSingleColor: AppThemeData.primary300, + buttonPadding: 10, + buttonWidth: 70, + pickerTitle: Text("", style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + backgroundColor: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + pickerTextStyle: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + closeIconColor: isDark ? Colors.white : Colors.black, + ).show(context); + }, + child: TextFieldWidget(title: "Booking Date & Slot".tr, hintText: "Choose Date and Time".tr, controller: controller.dateTimeController.value, enable: false), + ), + const SizedBox(height: 15), + controller.provider.value?.priceUnit == "Fixed" + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + controller.couponList.isNotEmpty + ? SizedBox( + height: 85, + child: ListView.builder( + itemCount: controller.couponList.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final coupon = controller.couponList[index]; + return GestureDetector(onTap: () => controller.applyCoupon(coupon), child: buildOfferItem(controller, index, isDark)); + }, + ), + ) + : Container(), + buildPromoCode(controller, isDark), + Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Text("Price Detail".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + priceTotalRow(controller, isDark), + ], + ) + : SizedBox(), + ], + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(20.0), + child: RoundedButtonFill(title: "Confirm".tr, color: AppThemeData.primary300, textColor: AppThemeData.grey50, onPress: () => controller.confirmBooking(context)), + ), + ); + }, + ); + } + + Widget buildOfferItem(OnDemandBookingController controller, int index, bool isDark) { + return Obx(() { + final coupon = controller.couponList[index]; + + return Container( + margin: const EdgeInsets.fromLTRB(7, 10, 7, 10), + height: 85, + child: DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(10), color: AppThemeData.primary300), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 5, 12, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Image(image: AssetImage('assets/images/offer_icon.png'), height: 25, width: 25), + const SizedBox(width: 10), + Container( + margin: const EdgeInsets.only(top: 3), + child: Text( + coupon.discountType == "Fix Price" ? "${Constant.amountShow(amount: coupon.discount.toString())} ${'OFF'.tr}" : "${coupon.discount} ${'% Off'.tr}", + style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.7, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + ], + ), + const SizedBox(height: 5), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(coupon.code ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal, letterSpacing: 0.5, color: Colors.orange)), + Container(margin: const EdgeInsets.only(left: 15, right: 15, top: 3), width: 1, color: AppThemeData.grey50), + Text( + "valid till ".tr + controller.getDate(coupon.expiresAt!.toDate().toString()), + style: TextStyle(letterSpacing: 0.5, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ], + ), + ), + ), + ); + }); + } + + Widget buildPromoCode(OnDemandBookingController controller, bool isDark) { + return GestureDetector( + child: Container( + margin: const EdgeInsets.only(top: 10, bottom: 13), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Image.asset("assets/images/reedem.png", height: 50, width: 50), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Promo Code".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), overflow: TextOverflow.ellipsis), + const SizedBox(height: 5), + Text( + "Apply promo code".tr, + style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + FloatingActionButton( + onPressed: () { + Get.bottomSheet(promoCodeSheet(controller, isDark), isScrollControlled: true, isDismissible: true, backgroundColor: Colors.transparent, enableDrag: true); + }, + mini: true, + backgroundColor: Colors.blueGrey.shade50, + elevation: 0, + child: const Icon(Icons.add, color: Colors.black54), + ), + ], + ), + ), + ), + ); + } + + Widget promoCodeSheet(OnDemandBookingController controller, bool isDark) { + return Container( + padding: EdgeInsets.only(bottom: Get.height / 4.3, left: 25, right: 25), + height: Get.height * 0.88, + decoration: BoxDecoration(color: Colors.transparent, border: Border.all(style: BorderStyle.none)), + child: Column( + children: [ + InkWell( + onTap: () => Get.back(), + child: Container( + height: 45, + decoration: BoxDecoration( + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100, width: 0.3), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + shape: BoxShape.circle, + ), + child: Center( + child: Icon( + Icons.close, + color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, // ✅ visible color + size: 28, + ), + ), + ), + ), + const SizedBox(height: 25), + Expanded( + child: Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50), + alignment: Alignment.center, + child: SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 30), child: const Image(image: AssetImage('assets/images/redeem_coupon.png'), width: 100)), + Container( + padding: const EdgeInsets.only(top: 20), + child: Text('Redeem Your Coupons'.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16)), + ), + Center( + child: Container( + padding: const EdgeInsets.only(top: 10, left: 22, right: 22), + child: Text("Voucher or Coupon code".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ), + Container( + padding: const EdgeInsets.only(left: 20, right: 20, top: 20), + child: DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(12), color: AppThemeData.primary300), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Container( + padding: const EdgeInsets.all(20), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + alignment: Alignment.center, + child: TextFormField( + textAlign: TextAlign.center, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + controller: controller.couponTextController.value, + decoration: InputDecoration( + border: InputBorder.none, + hintText: "Write Coupon Code".tr, + hintStyle: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400), + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 30, bottom: 30, left: 15, right: 15), + child: RoundedButtonFill( + title: "REDEEM NOW".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () { + final inputCode = controller.couponTextController.value.text.trim().toLowerCase(); + + final matchingCoupon = controller.couponList.firstWhereOrNull((c) => c.code?.toLowerCase() == inputCode); + + if (matchingCoupon != null) { + controller.applyCoupon(matchingCoupon); + Get.back(); + } else { + ShowToastDialog.showToast("Applied coupon not valid.".tr); + } + }, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget priceTotalRow(OnDemandBookingController controller, bool isDark) { + return Obx(() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Column( + children: [ + const SizedBox(height: 5), + rowText("Price".tr, Constant.amountShow(amount: controller.price.value.toString()), isDark), + controller.discountAmount.value != 0 ? const Divider() : const SizedBox(), + controller.discountAmount.value != 0 + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${"Discount".tr} ${controller.discountType.value == 'Percentage' || controller.discountType.value == 'Percent' ? "(${controller.discountLabel.value}%)" : "(${Constant.amountShow(amount: controller.discountLabel.value)})"}", + style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Text(controller.offerCode.value, style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ), + Text("(-${Constant.amountShow(amount: controller.discountAmount.value.toString())})", style: const TextStyle(color: Colors.red)), + ], + ), + ) + : const SizedBox(), + const Divider(), + rowText("SubTotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark), + const Divider(), + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "${taxModel.title} (${taxModel.type == "fix" ? Constant.amountShow(amount: taxModel.tax) : "${taxModel.tax}%"})", + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + Constant.amountShow(amount: Constant.getTaxValue(amount: controller.subTotal.value.toString(), taxModel: taxModel).toString()), + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + const Divider(), + ], + ); + }, + ), + rowText("Total Amount".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark), + const SizedBox(height: 5), + ], + ), + ); + }); + } + + Widget rowText(String title, String value, bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(value.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_category_screen.dart b/lib/screen_ui/on_demand_service/on_demand_category_screen.dart new file mode 100644 index 0000000..951ab38 --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_category_screen.dart @@ -0,0 +1,101 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/screen_ui/on_demand_service/view_category_service_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/on_demand_category_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/category_model.dart'; +import '../../themes/app_them_data.dart'; + +class OnDemandCategoryScreen extends StatelessWidget { + const OnDemandCategoryScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: OnDemandCategoryController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Explore services".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + Text( + "Explore services tailored for you—quick, easy, and personalized.".tr, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + child: SingleChildScrollView( + child: Column( + children: [ + controller.categories.isEmpty + ? Center(child: Text("No Categories".tr)) + : GridView.builder( + padding: const EdgeInsets.all(5), + itemCount: controller.categories.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + itemBuilder: (context, index) { + return categoriesCell(context, controller.categories[index], index, isDark); + }, + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget categoriesCell(BuildContext context, CategoryModel category, int index, bool isDark) { + return GestureDetector( + onTap: () { + Get.to(() => ViewCategoryServiceListScreen(), arguments: {'categoryId': category.id, 'categoryTitle': category.title}); + }, + child: Column( + children: [ + ClipRRect(borderRadius: BorderRadius.circular(12), child: CachedNetworkImage(imageUrl: category.image ?? "", height: 60, width: 60, fit: BoxFit.cover)), + const SizedBox(height: 5), + Text(category.title ?? "", style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), textAlign: TextAlign.center), + ], + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_dashboard_screen.dart b/lib/screen_ui/on_demand_service/on_demand_dashboard_screen.dart new file mode 100644 index 0000000..74e2b49 --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_dashboard_screen.dart @@ -0,0 +1,85 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cab_dashboard_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +import '../../controllers/on_demand_dashboard_controller.dart'; + +class OnDemandDashboardScreen extends StatelessWidget { + const OnDemandDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: OnDemandDashboardController(), + builder: (controller) { + return Scaffold( + body: controller.pageList[controller.selectedIndex.value], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + showUnselectedLabels: true, + showSelectedLabels: true, + selectedFontSize: 12, + selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + currentIndex: controller.selectedIndex.value, + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + selectedItemColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + unselectedItemColor: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + onTap: (int index) { + if (index == 0) { + Get.put(CabDashboardController()); + } + controller.selectedIndex.value = index; + }, + items: + Constant.walletSetting == false + ? [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ] + : [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller), + + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_wallet_cab.svg", label: 'Wallet'.tr, controller: controller), + navigationBarItem(isDark, index: 4, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ], + ), + ); + }, + ); + }); + } + + BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required OnDemandDashboardController controller}) { + return BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: SvgPicture.asset( + assetIcon, + height: 22, + width: 22, + color: + controller.selectedIndex.value == index + ? isDark + ? AppThemeData.primary300 + : AppThemeData.primary300 + : isDark + ? AppThemeData.grey300 + : AppThemeData.grey600, + ), + ), + label: label, + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_details_screen.dart b/lib/screen_ui/on_demand_service/on_demand_details_screen.dart new file mode 100644 index 0000000..98997ee --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_details_screen.dart @@ -0,0 +1,499 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/on_demand_service/provider_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/provider_serivce_model.dart'; +import '../../controllers/on_demand_details_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../auth_screens/login_screen.dart'; +import 'on_demand_booking_screen.dart'; + +class OnDemandDetailsScreen extends StatelessWidget { + const OnDemandDetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: OnDemandDetailsController(), + builder: (controller) { + return Scaffold( + body: buildSliverScrollView(context, controller, controller.provider, controller.userModel, isDark), + bottomNavigationBar: + controller.isOpen.value == false + ? SizedBox() + : Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedButtonFill( + title: "Book Now".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + if (Constant.userModel == null) { + Get.offAll(const LoginScreen()); + } else { + print("providerModel ::::::::${controller.provider.title ?? 'No provider'}"); + print("categoryTitle ::::::: ${controller.categoryTitle.value}"); + Get.to(() => OnDemandBookingScreen(), arguments: {'providerModel': controller.provider, 'categoryTitle': controller.categoryTitle.value}); + } + }, + ), + ], + ), + ), + ); + }, + ); + } + + SingleChildScrollView buildSliverScrollView(BuildContext context, OnDemandDetailsController controller, ProviderServiceModel provider, user, isDark) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + CachedNetworkImage( + imageUrl: provider.photos.isNotEmpty ? provider.photos.first : "", + placeholder: (context, url) => Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.fitWidth), + fit: BoxFit.fitWidth, + width: width, + height: height * 0.45, + ), + Positioned(top: height * 0.05, left: width * 0.03, child: _circleButton(context, icon: Icons.arrow_back, onTap: () => Get.back())), + Positioned( + top: height * 0.05, + right: width * 0.03, + child: Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(40), color: controller.isOpen.value ? Colors.green : Colors.red), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text(controller.isOpen.value ? "Open".tr : "Close".tr, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 14)), + ), + ), + ], + ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + child: GetBuilder( + builder: (controller) { + final provider = controller.provider; + final categoryTitle = controller.categoryTitle.value; + final subCategoryTitle = controller.subCategoryTitle.value; + // final tabString = controller.tabString.value; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + provider.title.toString(), + style: TextStyle(fontSize: 20, fontFamily: AppThemeData.regular, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), + ), + ), + Row( + children: [ + provider.disPrice == "" || provider.disPrice == "0" + ? Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price ?? '0') : '${Constant.amountShow(amount: provider.price ?? '0')}/${'hr'.tr}', + style: TextStyle(fontSize: 18, fontFamily: AppThemeData.regular, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300), + ) + : Row( + children: [ + Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.disPrice ?? '0') : '${Constant.amountShow(amount: provider.disPrice ?? '0')}/${'hr'.tr}', + style: TextStyle(fontSize: 18, fontFamily: AppThemeData.regular, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price ?? '0') : '${Constant.amountShow(amount: provider.price ?? '0')}/${'hr'.tr}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.grey, decoration: TextDecoration.lineThrough), + ), + ), + ], + ), + ], + ), + ], + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Text(categoryTitle, style: TextStyle(fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400, color: isDark ? Colors.white : Colors.black)), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Row( + children: [ + const Icon(Icons.star, size: 16, color: AppThemeData.warning400), + const SizedBox(width: 3), + Text( + provider.reviewsCount != 0 ? ((provider.reviewsSum ?? 0.0) / (provider.reviewsCount ?? 0.0)).toStringAsFixed(1) : '0', + style: const TextStyle(letterSpacing: 0.5, fontSize: 16, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: AppThemeData.warning400), + ), + const SizedBox(width: 10), + Text( + "(${provider.reviewsCount} ${'Reviews'.tr})", + style: TextStyle(letterSpacing: 0.5, fontSize: 16, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: isDark ? Colors.white : Colors.black), + ), + ], + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + subCategoryTitle.isNotEmpty + ? Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.primary300.withOpacity(0.20)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text(subCategoryTitle, style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, fontFamily: AppThemeData.regular, color: AppThemeData.primary300)), + ), + ) + : Container(), + const SizedBox(width: 10), + Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: Colors.green.withOpacity(0.20)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: InkWell( + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + isDismissible: true, + context: context, + backgroundColor: Colors.transparent, + enableDrag: true, + builder: (context) => showTiming(context, controller, isDark), + ); + }, + child: Text("View Timing".tr, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.green, letterSpacing: 0.5)), + ), + ), + ), + ], + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.location_on_outlined, color: isDark ? Colors.white : Colors.black, size: 20), + const SizedBox(width: 5), + Expanded( + child: Text( + provider.address.toString(), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400, color: isDark ? Colors.white : Colors.black), + ), + ), + ], + ), + + const SizedBox(height: 10), + const Divider(), + _tabBar(controller), + Obx(() { + if (controller.tabString.value == "About") { + return aboutTabViewWidget(controller, controller.provider, isDark); + } else if (controller.tabString.value == "Gallery") { + return galleryTabViewWidget(controller); + } else { + return reviewTabViewWidget(controller, isDark); + } + }), + const SizedBox(height: 15), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget _circleButton(BuildContext context, {required IconData icon, required VoidCallback onTap}) { + return ClipOval( + child: Container(color: Colors.black.withOpacity(0.7), child: InkWell(onTap: onTap, child: Padding(padding: const EdgeInsets.all(8.0), child: Icon(icon, size: 30, color: Colors.white)))), + ); + } + + Widget _tabBar(OnDemandDetailsController controller) { + return Obx(() => Row(children: [_tabItem("About", controller), _tabItem("Gallery", controller), _tabItem("Review", controller)])); + } + + Widget _tabItem(String title, OnDemandDetailsController controller) { + return GestureDetector( + onTap: () => controller.changeTab(title), + child: Container( + margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration(color: controller.tabString.value == title ? AppThemeData.primary300 : Colors.grey.shade200, borderRadius: BorderRadius.circular(10)), + child: Text(title.tr, style: TextStyle(fontWeight: FontWeight.bold, color: controller.tabString.value == title ? Colors.white : Colors.black)), + ), + ); + } + + Widget aboutTabViewWidget(OnDemandDetailsController controller, ProviderServiceModel providerModel, bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text((providerModel.description ?? '').tr, style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500)), + const SizedBox(height: 10), + Obx(() { + final user = controller.userModel.value; + if (user == null) return const SizedBox(); + return InkWell( + onTap: () { + Get.to(() => ProviderScreen(), arguments: {'providerId': user.id}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1), + color: isDark ? AppThemeData.grey500 : Colors.white, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + CircleAvatar(radius: 30, backgroundImage: NetworkImage(user.profilePictureURL?.isNotEmpty == true ? user.profilePictureURL! : Constant.placeHolderImage)), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(user.fullName(), style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.bold)), + const SizedBox(height: 5), + Text(user.email ?? '', style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14)), + const SizedBox(height: 10), + // Rating Box + Container( + decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 16, color: Colors.white), + const SizedBox(width: 3), + Text( + double.parse(user.reviewsCount.toString()) != 0 + ? (double.parse(user.reviewsSum.toString()) / double.parse(user.reviewsCount.toString())).toStringAsFixed(1) + : '0', + style: const TextStyle(letterSpacing: 0.5, fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: Colors.white), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right), + ], + ), + ), + ), + ), + ); + }), + ], + ), + ); + } + + Widget galleryTabViewWidget(OnDemandDetailsController controller) { + final photos = controller.provider.photos; + + if (photos.isEmpty) { + return Center(child: Text("No Image Found".tr)); + } + + return GridView.builder( + itemCount: photos.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, mainAxisSpacing: 0, crossAxisSpacing: 8, mainAxisExtent: 180), + itemBuilder: (context, index) { + final imageUrl = photos[index]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: imageUrl, + height: 60, + width: 60, + imageBuilder: (context, imageProvider) => Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover), + fit: BoxFit.cover, + ), + ), + ); + }, + ); + } + + Widget reviewTabViewWidget(OnDemandDetailsController controller, bool isDark) { + final reviews = controller.ratingService; + + if (reviews.isEmpty) { + return SizedBox(height: 200, child: Center(child: Text("No review Found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)))); + } + + return ListView.builder( + itemCount: reviews.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final review = reviews[index]; + return Padding( + padding: const EdgeInsets.only(top: 10), + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: isDark ? AppThemeData.grey700 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shadows: const [BoxShadow(color: Color(0x0A000000), blurRadius: 32, offset: Offset(0, 0), spreadRadius: 0)], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(review.uname ?? '', style: TextStyle(fontSize: 16, letterSpacing: 1, fontWeight: FontWeight.w600, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + review.createdAt != null ? DateFormat('dd MMM').format(review.createdAt!.toDate()) : '', + style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + const SizedBox(height: 4), + RatingBar.builder( + initialRating: double.tryParse(review.rating.toString()) ?? 0, + direction: Axis.horizontal, + itemSize: 20, + ignoreGestures: true, + itemPadding: const EdgeInsets.symmetric(horizontal: 4.0), + itemBuilder: (context, _) => Icon(Icons.star, color: AppThemeData.primary300), + onRatingUpdate: (rate) {}, + ), + const Divider(), + const SizedBox(height: 5), + Text(review.comment ?? '', style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ), + ), + ); + }, + ); + } + + Widget showTiming(BuildContext context, OnDemandDetailsController controller, bool isDark) { + final provider = controller.provider; + return Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.grey300 : Colors.white, borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20))), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + child: Text("Service Timing".tr, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, fontFamily: AppThemeData.regular, color: AppThemeData.primary300)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + child: Row( + children: [ + Expanded(child: _timeCard(context, "Start Time : ".tr, provider.startTime.toString(), isDark)), + const SizedBox(width: 10), + Expanded(child: _timeCard(context, "End Time : ".tr, provider.endTime.toString(), isDark)), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + child: Text("Service Days".tr, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, fontFamily: AppThemeData.regular, color: AppThemeData.primary300)), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 6.0, + runSpacing: 6.0, + children: + provider.days + .map( + (day) => Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6), side: BorderSide(color: isDark ? const Color(0XFF3c3a2e) : const Color(0XFFC3C5D1), width: 1)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 20), + child: Text(day, style: TextStyle(color: isDark ? const Color(0XFFa5a292) : const Color(0XFF5A5D6D))), + ), + ), + ) + .toList(), + ), + ), + const SizedBox(height: 10), + ], + ), + ); + } + + Widget _timeCard(BuildContext context, String title, String value, bool isDark) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6), side: BorderSide(color: isDark ? const Color(0XFF3c3a2e) : const Color(0XFFC3C5D1), width: 1)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 20), + child: Row( + children: [ + Text(title, style: TextStyle(color: isDark ? const Color(0XFFa5a292) : const Color(0XFF5A5D6D))), + Text(value, style: TextStyle(color: isDark ? const Color(0XFFa5a292) : const Color(0XFF5A5D6D))), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_favourites_screen.dart b/lib/screen_ui/on_demand_service/on_demand_favourites_screen.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_favourites_screen.dart @@ -0,0 +1 @@ + diff --git a/lib/screen_ui/on_demand_service/on_demand_home_screen.dart b/lib/screen_ui/on_demand_service/on_demand_home_screen.dart new file mode 100644 index 0000000..0a7f050 --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_home_screen.dart @@ -0,0 +1,532 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/banner_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart'; +import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart'; +import 'package:customer/screen_ui/on_demand_service/view_all_popular_service_screen.dart'; +import 'package:customer/screen_ui/on_demand_service/view_category_service_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/widget/osm_map/map_picker_page.dart'; +import 'package:customer/widget/place_picker/location_picker_screen.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import '../../controllers/on_demand_home_controller.dart'; +import '../../models/category_model.dart'; +import '../../models/provider_serivce_model.dart'; +import 'on_demand_category_screen.dart'; +import 'on_demand_details_screen.dart'; + +class OnDemandHomeScreen extends StatelessWidget { + const OnDemandHomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: OnDemandHomeController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: const BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: const Center(child: Padding(padding: EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell(onTap: () => Get.offAll(const LoginScreen()), child: Text("Login".tr, style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 12))) + : Text(Constant.userModel!.fullName(), style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 12)), + InkWell( + onTap: () async { + if (Constant.userModel != null) { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } else { + Constant.checkPermission( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + // ✅ declare it once here! + ShippingAddress shippingAddress = ShippingAddress(); + + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(); + ShowToastDialog.closeLoader(); + + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + shippingAddress.addressAs = "Home"; + shippingAddress.locality = address.toString(); + shippingAddress.location = UserLocation(latitude: lat, longitude: lng); + Constant.selectedLocation = shippingAddress; + controller.getData(); + Get.back(); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + shippingAddress.locality = "Picked from Map"; // You can reverse-geocode + + Constant.selectedLocation = shippingAddress; + controller.getData(); + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + shippingAddress.locality = currentLocation; + }); + + Constant.selectedLocation = shippingAddress; + ShowToastDialog.closeLoader(); + controller.getData(); + } + }, + context: context, + ); + } + }, + child: Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + TextSpan( + text: Constant.selectedLocation.getFullAddress(), + style: TextStyle(fontFamily: AppThemeData.medium, overflow: TextOverflow.ellipsis, color: AppThemeData.grey900, fontSize: 14), + ), + WidgetSpan(child: SvgPicture.asset("assets/icons/ic_down.svg")), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Constant.isZoneAvailable == false || controller.providerList.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/location.gif", height: 120), + const SizedBox(height: 12), + Text("No Store Found in Your Area".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "Currently, there are no available store in your zone. Try changing your location to find nearby options.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Change Zone".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LocationPermissionScreen()); + }, + ), + ], + ), + ) + : Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BannerView(bannerList: controller.bannerTopHome), + const SizedBox(height: 20), + Container( + height: MediaQuery.of(context).size.height * 0.12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark600 : AppThemeData.greyDark600, width: 1), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: + controller.categories.isEmpty + ? Constant.showEmptyView(message: "No Categories".tr) + : Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: ListView.builder( + itemCount: controller.categories.length > 3 ? 3 : controller.categories.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final category = controller.categories[index]; + return InkWell( + onTap: () { + Get.to(() => ViewCategoryServiceListScreen(), arguments: {'categoryId': category.id, 'categoryTitle': category.title}); + }, + child: CategoryView(category: category, index: index, isDark: isDark), + ); + }, + ), + ), + if (controller.categories.length > 3) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () { + Get.to(() => const OnDemandCategoryScreen()); + }, + child: ClipOval(child: Container(width: 50, height: 50, color: AppThemeData.grey200, child: const Center(child: Icon(Icons.chevron_right)))), + ), + const SizedBox(height: 5), + SizedBox( + width: 70, + child: Center( + child: Text( + "View All".tr, + textAlign: TextAlign.center, + maxLines: 1, + style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 10), + child: Row( + children: [ + Expanded( + child: Text( + "Most Popular services".tr, + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 18, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w600), + ), + ), + InkWell( + onTap: () { + Get.to(() => ViewAllPopularServiceScreen()); + }, + child: Text("View all".tr, style: TextStyle(color: AppThemeData.primary300, fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w600)), + ), + ], + ), + ), + controller.providerList.isEmpty + ? Center(child: Text("No Services Found".tr)) + : ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.providerList.length >= 6 ? 6 : controller.providerList.length, + itemBuilder: (_, index) { + return ServiceView(provider: controller.providerList[index], controller: controller, isDark: isDark); + }, + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class BannerView extends StatelessWidget { + final List bannerList; + final RxInt currentPage = 0.obs; + final ScrollController scrollController = ScrollController(); + + BannerView({super.key, required this.bannerList}); + + void onScroll(BuildContext context) { + if (scrollController.hasClients && bannerList.isNotEmpty) { + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = screenWidth * 0.8 + 10; // banner width + spacing + final offset = scrollController.offset; + final index = (offset / itemWidth).round(); + + if (index != currentPage.value && index < bannerList.length) { + currentPage.value = index; + } + } + } + + @override + Widget build(BuildContext context) { + scrollController.addListener(() { + onScroll(context); + }); + + return Column( + children: [ + SizedBox( + height: 150, + child: ListView.separated( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: bannerList.length, + separatorBuilder: (context, index) => const SizedBox(width: 15), + itemBuilder: (context, index) { + final banner = bannerList[index]; + return ClipRRect( + borderRadius: BorderRadius.circular(15), + child: SizedBox(width: MediaQuery.of(context).size.width * 0.8, child: NetworkImageWidget(imageUrl: banner.photo ?? '', fit: BoxFit.cover)), + ); + }, + ), + ), + const SizedBox(height: 8), + Obx(() { + return Row( + children: List.generate(bannerList.length, (index) { + bool isSelected = currentPage.value == index; + return Expanded(child: Container(height: 4, decoration: BoxDecoration(color: isSelected ? AppThemeData.grey300 : AppThemeData.grey100, borderRadius: BorderRadius.circular(5)))); + }), + ); + }), + ], + ); + } +} + +class CategoryView extends StatelessWidget { + final CategoryModel category; + final int index; + final bool isDark; + + const CategoryView({super.key, required this.category, required this.index, required this.isDark}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 55, + width: 55, + decoration: BoxDecoration(color: Constant.colorList[index % Constant.colorList.length], borderRadius: BorderRadius.circular(50)), + child: ClipOval( + child: Padding( + padding: const EdgeInsets.all(14.0), + child: CachedNetworkImage(imageUrl: category.image.toString(), errorWidget: (_, __, ___) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover)), + ), + ), + ), + const SizedBox(height: 5), + SizedBox( + width: 70, + child: Center( + child: Text(category.title ?? "", textAlign: TextAlign.center, maxLines: 1, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ), + ], + ), + ); + } +} + +class ServiceView extends StatelessWidget { + final ProviderServiceModel provider; + final bool isDark; + final OnDemandHomeController? controller; + + const ServiceView({super.key, required this.provider, this.isDark = false, this.controller}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Get.to(() => OnDemandDetailsScreen(), arguments: {'providerModel': provider}); + }, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade200), + color: isDark ? AppThemeData.grey900 : Colors.white, + ), + child: Row( + children: [ + // --- Left Image --- + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + child: CachedNetworkImage( + imageUrl: provider.photos.isNotEmpty ? provider.photos[0] : Constant.placeHolderImage, + width: 110, + height: MediaQuery.of(context).size.height * 0.16, + fit: BoxFit.cover, + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover), + ), + ), + + // --- Right Content --- + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + Favourite icon + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + provider.title ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), + ), + ), + if (controller != null) + Obx( + () => GestureDetector( + onTap: () => controller!.toggleFavourite(provider), + child: Icon( + controller!.lstFav.where((element) => element.service_id == provider.id).isNotEmpty ? Icons.favorite : Icons.favorite_border, + size: 24, + color: controller!.lstFav.where((element) => element.service_id == provider.id).isNotEmpty ? AppThemeData.primary300 : (isDark ? Colors.white38 : Colors.black38), + ), + ), + ), + ], + ), + + const SizedBox(height: 4), + + // Category + if (controller != null) + FutureBuilder( + future: controller!.getCategory(provider.categoryId ?? ""), + builder: (ctx, snap) { + if (!snap.hasData) return const SizedBox.shrink(); + return Text(snap.data?.title ?? "", style: TextStyle(fontSize: 13, color: isDark ? Colors.white70 : Colors.black54)); + }, + ), + + const SizedBox(height: 4), + + // Price + _buildPrice(), + + const SizedBox(height: 6), + + // Rating + _buildRating(), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPrice() { + if (provider.disPrice == "" || provider.disPrice == "0") { + return Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/${'hr'.tr}', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300), + ); + } else { + return Row( + children: [ + Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.disPrice ?? '0') : '${Constant.amountShow(amount: provider.disPrice)}/${'hr'.tr}', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/hr', + style: const TextStyle(fontSize: 12, color: Colors.grey, decoration: TextDecoration.lineThrough), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + } + + Widget _buildRating() { + double rating = 0; + if (provider.reviewsCount != null && provider.reviewsCount != 0) { + rating = (provider.reviewsSum ?? 0) / (provider.reviewsCount ?? 1); + } + return Container( + decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [const Icon(Icons.star, size: 14, color: Colors.white), const SizedBox(width: 3), Text(rating.toStringAsFixed(1), style: const TextStyle(fontSize: 12, color: Colors.white))], + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_order_details_screen.dart b/lib/screen_ui/on_demand_service/on_demand_order_details_screen.dart new file mode 100644 index 0000000..f7723bc --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_order_details_screen.dart @@ -0,0 +1,1043 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../../controllers/on_demand_order_details_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/tax_model.dart'; +import '../../themes/app_them_data.dart'; +import '../../constant/constant.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../multi_vendor_service/chat_screens/chat_screen.dart'; +import 'on_demand_payment_screen.dart'; +import 'on_demand_review_screen.dart'; + +class OnDemandOrderDetailsScreen extends StatelessWidget { + const OnDemandOrderDetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: OnDemandOrderDetailsController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Order Details".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (controller.onProviderOrder.value?.status ?? '') == Constant.orderCancelled + ? Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Cancel Reason'.tr, style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(controller.onProviderOrder.value?.reason ?? '', style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.danger300)), + ], + ), + ), + ) + : Container(), + (controller.onProviderOrder.value?.status ?? '') == Constant.orderCancelled ? SizedBox(height: 10) : SizedBox.shrink(), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Booking ID'.tr, style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + InkWell( + onTap: () { + FlutterClipboard.copy(controller.onProviderOrder.value?.id ?? '').then((value) { + SnackBar snackBar = SnackBar( + content: Text( + "Booking ID Copied".tr, + textAlign: TextAlign.center, + style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + backgroundColor: Colors.black38, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }); + }, + child: Text('# ${controller.onProviderOrder.value?.id ?? ''}', style: AppThemeData.mediumTextStyle(fontSize: 15, color: AppThemeData.primary300)), + ), + ], + ), + SizedBox(height: 10), + Text( + "${'Booking Address :'.tr} ${controller.onProviderOrder.value?.address?.getFullAddress()}", + style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ), + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + height: 80, + width: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + image: NetworkImage( + (controller.onProviderOrder.value != null && controller.onProviderOrder.value!.provider.photos.isNotEmpty) + ? controller.onProviderOrder.value!.provider.photos.first + : Constant.placeHolderImage, + ), + fit: BoxFit.cover, + ), + ), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + controller.onProviderOrder.value?.provider.title ?? "", + style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text('${'Date:'.tr} ', style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + controller.onProviderOrder.value?.scheduleDateTime != null + ? DateFormat('dd-MMM-yyyy').format(controller.onProviderOrder.value!.scheduleDateTime!.toDate()) + : "", + style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + ], + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text('${'Time:'.tr} ', style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + controller.onProviderOrder.value?.scheduleDateTime != null + ? DateFormat('hh:mm a').format(controller.onProviderOrder.value!.scheduleDateTime!.toDate()) + : "", + style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + (controller.onProviderOrder.value?.status == Constant.orderAccepted || + controller.onProviderOrder.value?.status == Constant.orderAssigned || + controller.onProviderOrder.value?.status == Constant.orderOngoing || + controller.onProviderOrder.value?.status == Constant.orderCompleted) && + (controller.onProviderOrder.value?.workerId != null && controller.onProviderOrder.value!.workerId!.isNotEmpty) + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Text('About Worker'.tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + CircleAvatar( + radius: 30, + backgroundImage: NetworkImage( + controller.worker.value?.profilePictureURL.isNotEmpty == true ? controller.worker.value!.profilePictureURL : Constant.placeHolderImage, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.worker.value?.fullName() ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 3), + child: Icon(Icons.location_on_outlined, size: 15, color: isDark ? Colors.white : Colors.black), + ), + Expanded( + child: Text( + controller.worker.value?.address ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ], + ), + ), + SizedBox(width: 10), + // Rating Box + Container( + decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 16, color: Colors.white), + const SizedBox(width: 3), + Text( + (controller.worker.value != null && double.parse(controller.worker.value!.reviewsCount.toString()) != 0) + ? (double.parse(controller.worker.value!.reviewsSum.toString()) / double.parse(controller.worker.value!.reviewsCount.toString())) + .toStringAsFixed(1) + : '0', + style: const TextStyle(letterSpacing: 0.5, fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: Colors.white), + ), + ], + ), + ), + ], + ), + Visibility( + visible: controller.onProviderOrder.value?.status == Constant.orderCompleted ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: ElevatedButton( + // onPressed: () async { + // Get.to(() => OnDemandReviewScreen(), arguments: {'order': controller.onProviderOrder.value, 'reviewFor': "Worker"}); + // }, + onPressed: () async { + final result = await Get.to(() => OnDemandReviewScreen(), arguments: {'order': controller.onProviderOrder.value, 'reviewFor': "Worker"}); + + // If review was submitted successfully + if (result == true) { + await controller.getData(); + } + }, + + style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Text('Add Review'.tr, style: AppThemeData.regularTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ), + ), + ), + controller.onProviderOrder.value?.status == Constant.orderAccepted || + controller.onProviderOrder.value?.status == Constant.orderOngoing || + controller.onProviderOrder.value?.status == Constant.orderAssigned + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + Constant.makePhoneCall(controller.worker.value!.phoneNumber.toString()); + }, + style: ElevatedButton.styleFrom(backgroundColor: Color(0xFFFF6839), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.call, color: AppThemeData.grey50), + SizedBox(width: 10), + Text('Call'.tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.grey50)), + ], + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton( + onPressed: () async { + ShowToastDialog.showLoader("Please wait...".tr); + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": Constant.userModel?.fullName(), + "restaurantName": "${controller.worker.value?.firstName ?? ''} ${controller.worker.value?.lastName ?? ''}", + "orderId": controller.onProviderOrder.value?.id, + "restaurantId": controller.worker.value?.id, + "customerId": Constant.userModel?.id, + "customerProfileImage": Constant.userModel?.profilePictureURL, + "restaurantProfileImage": controller.worker.value?.profilePictureURL, + "token": controller.worker.value?.fcmToken, + "chatType": 'worker', + }, + ); + }, + style: ElevatedButton.styleFrom(backgroundColor: AppThemeData.primary300, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.chat_bubble, color: AppThemeData.grey50), + SizedBox(width: 10), + Text('Chat'.tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.grey50)), + ], + ), + ), + ), + ], + ), + ) + : SizedBox(), + ], + ), + ), + ), + ], + ) + : SizedBox(), + Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Text("About provider".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + CircleAvatar( + radius: 30, + backgroundImage: NetworkImage( + controller.providerUser.value?.profilePictureURL?.isNotEmpty == true ? controller.providerUser.value!.profilePictureURL! : Constant.placeHolderImage, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.providerUser.value?.fullName() ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + Text( + controller.providerUser.value?.email ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14), + ), + const SizedBox(height: 10), + ], + ), + ), + ], + ), + ), + // Rating Box + Container( + decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 16, color: Colors.white), + const SizedBox(width: 3), + Text( + (controller.providerUser.value != null && double.parse(controller.providerUser.value!.reviewsCount.toString()) != 0) + ? (double.parse(controller.providerUser.value!.reviewsSum.toString()) / double.parse(controller.providerUser.value!.reviewsCount.toString())) + .toStringAsFixed(1) + : '0', + style: const TextStyle(letterSpacing: 0.5, fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: Colors.white), + ), + ], + ), + ), + ], + ), + Visibility( + visible: controller.onProviderOrder.value?.status == Constant.orderCompleted ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: ElevatedButton( + onPressed: () async { + final result = await Get.to(() => OnDemandReviewScreen(), arguments: {'order': controller.onProviderOrder.value, 'reviewFor': "Provider"}); + if (result == true) { + await controller.getData(); + } + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Text('Add Review'.tr, style: AppThemeData.regularTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ), + ), + ), + controller.onProviderOrder.value?.status == Constant.orderAccepted || + controller.onProviderOrder.value?.status == Constant.orderOngoing || + controller.onProviderOrder.value?.status == Constant.orderAssigned + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + Constant.makePhoneCall(controller.providerUser.value!.phoneNumber.toString()); + }, + style: ElevatedButton.styleFrom(backgroundColor: Color(0xFFFF6839), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.call, color: AppThemeData.grey50), + SizedBox(width: 10), + Text('Call'.tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.grey50)), + ], + ), + ), + ), + if ((Constant.isSubscriptionModelApplied == false && Constant.sectionConstantModel?.adminCommision?.isEnabled == false) || + ((Constant.isSubscriptionModelApplied == true || Constant.sectionConstantModel?.adminCommision?.isEnabled == true) && + controller.onProviderOrder.value?.provider.subscriptionPlan?.features?.chat == true)) + const SizedBox(width: 10), + if ((Constant.isSubscriptionModelApplied == false && Constant.sectionConstantModel?.adminCommision?.isEnabled == false) || + ((Constant.isSubscriptionModelApplied == true || Constant.sectionConstantModel?.adminCommision?.isEnabled == true) && + controller.onProviderOrder.value?.provider.subscriptionPlan?.features?.chat == true)) + Expanded( + child: ElevatedButton( + onPressed: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": Constant.userModel?.fullName(), + "restaurantName": "${controller.providerUser.value?.firstName ?? ''} ${controller.providerUser.value?.lastName ?? ''}", + "orderId": controller.onProviderOrder.value?.id, + "restaurantId": controller.providerUser.value?.id, + "customerId": Constant.userModel?.id, + "customerProfileImage": Constant.userModel?.profilePictureURL, + "restaurantProfileImage": controller.providerUser.value?.profilePictureURL, + "token": controller.providerUser.value?.fcmToken, + "chatType": 'Provider', + }, + ); + }, + style: ElevatedButton.styleFrom(backgroundColor: AppThemeData.primary300, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.chat_bubble, color: AppThemeData.grey50), + SizedBox(width: 10), + Text('Chat'.tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: AppThemeData.grey50)), + ], + ), + ), + ), + ], + ), + ) + : SizedBox(), + ], + ), + ), + ), + (controller.onProviderOrder.value?.status != Constant.orderCompleted || controller.onProviderOrder.value?.status != Constant.orderCancelled) && + controller.onProviderOrder.value?.provider.priceUnit == "Fixed" + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Text("Price Detail".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + priceTotalRow(controller, isDark), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + controller.onProviderOrder.value?.paymentStatus == false || controller.onProviderOrder.value?.extraPaymentStatus == false + ? Column( + children: [ + controller.couponList.isNotEmpty + ? SizedBox( + height: 85, + child: ListView.builder( + itemCount: controller.couponList.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final coupon = controller.couponList[index]; + return GestureDetector(onTap: () => controller.applyCoupon(coupon), child: buildOfferItem(controller, index, isDark)); + }, + ), + ) + : Container(), + buildPromoCode(controller, isDark), + ], + ) + : Offstage(), + Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text("Price Detail".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + priceTotalRow(controller, isDark), + ], + ), + controller.onProviderOrder.value?.extraCharges.toString() != "" + ? Container( + margin: EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Total Extra Charges : ".tr, style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500)), + Text( + Constant.amountShow(amount: controller.onProviderOrder.value?.extraCharges.toString()), + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Extra charge Notes : ".tr, + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + ), + Text( + controller.onProviderOrder.value?.extraChargesDescription ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + ], + ), + ], + ), + ), + ) + : SizedBox(), + SizedBox(height: 10), + Visibility( + visible: controller.onProviderOrder.value?.status == Constant.orderPlaced || controller.onProviderOrder.value?.newScheduleDateTime != null ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Column( + children: [ + controller.onProviderOrder.value?.newScheduleDateTime != null + ? Row( + children: [ + Text("New Date : ".tr, style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500)), + Text( + DateFormat('dd-MMM-yyyy hh:mm a').format(controller.onProviderOrder.value!.newScheduleDateTime!.toDate()), + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500), + ), + ], + ) + : SizedBox(), + controller.onProviderOrder.value?.status == Constant.orderPlaced || controller.onProviderOrder.value?.status == Constant.orderAccepted + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: "Cancel Booking".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () { + showCancelBookingDialog(controller, isDark); + }, + ), + ) + : SizedBox(), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 20), + controller.onProviderOrder.value?.extraPaymentStatus == false && controller.onProviderOrder.value?.status == Constant.orderOngoing + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: 'Pay Extra Amount'.tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + double finalTotalAmount = 0.0; + finalTotalAmount = double.parse(controller.onProviderOrder.value!.extraCharges.toString()); + Get.to(() => OnDemandPaymentScreen(), arguments: {'onDemandOrderModel': controller.onProviderOrder, 'totalAmount': finalTotalAmount, 'isExtra': true}); + }, + ), + ) + : SizedBox(), + controller.onProviderOrder.value?.provider.priceUnit != "Fixed" && controller.onProviderOrder.value?.paymentStatus == false + ? Visibility( + visible: controller.onProviderOrder.value?.status == Constant.orderOngoing ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: 'Pay Now'.tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + double finalTotalAmount = 0.0; + finalTotalAmount = + controller.totalAmount.value + + double.parse(controller.onProviderOrder.value!.extraCharges!.isNotEmpty ? controller.onProviderOrder.value!.extraCharges.toString() : "0.0"); + controller.onProviderOrder.value?.discount = controller.discountAmount.toString(); + controller.onProviderOrder.value?.discountType = controller.discountType.toString(); + controller.onProviderOrder.value?.discountLabel = controller.discountLabel.toString(); + controller.onProviderOrder.value?.couponCode = controller.offerCode.toString(); + + Get.to(() => OnDemandPaymentScreen(), arguments: {'onDemandOrderModel': controller.onProviderOrder, 'totalAmount': finalTotalAmount, 'isExtra': false}); + }, + ), + ), + ) + : SizedBox(), + ], + ), + ), + ); + }, + ); + } + + Widget buildOfferItem(OnDemandOrderDetailsController controller, int index, bool isDark) { + return Obx(() { + final coupon = controller.couponList[index]; + + return Container( + margin: const EdgeInsets.fromLTRB(7, 10, 7, 10), + height: 85, + child: DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(10), color: AppThemeData.primary300), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 5, 12, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Image(image: AssetImage('assets/images/offer_icon.png'), height: 25, width: 25), + const SizedBox(width: 10), + Container( + margin: const EdgeInsets.only(top: 3), + child: Text( + coupon.discountType == "Fix Price" ? "${Constant.amountShow(amount: coupon.discount.toString())} ${'OFF'.tr}" : "${coupon.discount} ${'% Off'.tr}", + style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.7, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + ], + ), + const SizedBox(height: 5), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(coupon.code ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal, letterSpacing: 0.5, color: Colors.orange)), + Container(margin: const EdgeInsets.only(left: 15, right: 15, top: 3), width: 1, color: AppThemeData.grey50), + Text( + "valid till ".tr + controller.getDate(coupon.expiresAt!.toDate().toString()), + style: TextStyle(letterSpacing: 0.5, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ], + ), + ), + ), + ); + }); + } + + Widget buildPromoCode(OnDemandOrderDetailsController controller, bool isDark) { + return GestureDetector( + child: Container( + margin: const EdgeInsets.only(top: 10, bottom: 13), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100, width: 0.3), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Image.asset("assets/images/reedem.png", height: 50, width: 50), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Promo Code".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), overflow: TextOverflow.ellipsis), + const SizedBox(height: 5), + Text( + "Apply promo code".tr, + style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + FloatingActionButton( + onPressed: () { + Get.bottomSheet(promoCodeSheet(controller, isDark), isScrollControlled: true, isDismissible: true, backgroundColor: Colors.transparent, enableDrag: true); + }, + mini: true, + backgroundColor: Colors.blueGrey.shade50, + elevation: 0, + child: const Icon(Icons.add, color: Colors.black54), + ), + ], + ), + ), + ), + ); + } + + Widget promoCodeSheet(OnDemandOrderDetailsController controller, bool isDark) { + return Container( + padding: EdgeInsets.only(bottom: Get.height / 4.3, left: 25, right: 25), + height: Get.height * 0.88, + decoration: BoxDecoration(color: Colors.transparent, border: Border.all(style: BorderStyle.none)), + child: Column( + children: [ + InkWell( + onTap: () => Get.back(), + child: Container( + height: 45, + decoration: BoxDecoration( + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100, width: 0.3), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + shape: BoxShape.circle, + ), + child: Center(child: Icon(Icons.close, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, size: 28)), + ), + ), + const SizedBox(height: 25), + Expanded( + child: Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50), + alignment: Alignment.center, + child: SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 30), child: const Image(image: AssetImage('assets/images/redeem_coupon.png'), width: 100)), + Container( + padding: const EdgeInsets.only(top: 20), + child: Text('Redeem Your Coupons'.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16)), + ), + Center( + child: Container( + padding: const EdgeInsets.only(top: 10, left: 22, right: 22), + child: Text("Voucher or Coupon code".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ), + Container( + padding: const EdgeInsets.only(left: 20, right: 20, top: 20), + child: DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(12), color: AppThemeData.primary300), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Container( + padding: const EdgeInsets.all(20), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + alignment: Alignment.center, + child: TextFormField( + textAlign: TextAlign.center, + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + controller: controller.couponTextController.value, + decoration: InputDecoration( + border: InputBorder.none, + hintText: "Write Coupon Code".tr, + hintStyle: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400), + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 30, bottom: 30, left: 15, right: 15), + child: RoundedButtonFill( + title: "REDEEM NOW".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () { + final inputCode = controller.couponTextController.value.text.trim().toLowerCase(); + print("Entered code: $inputCode"); + print("Available coupons: ${controller.couponList.map((e) => e.code).toList()}"); + + final matchingCoupon = controller.couponList.firstWhereOrNull((c) => (c.code ?? '').trim().toLowerCase() == inputCode); + + if (matchingCoupon != null) { + print("✅ Coupon matched: ${matchingCoupon.code}"); + controller.applyCoupon(matchingCoupon); + Future.delayed(const Duration(milliseconds: 300), () { + Get.back(); + }); + } else { + print("❌ No matching coupon found"); + ShowToastDialog.showToast("Applied coupon not valid.".tr); + } + }, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget priceTotalRow(OnDemandOrderDetailsController controller, bool isDark) { + return Obx(() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + ), + child: Column( + children: [ + const SizedBox(height: 5), + rowText( + "Price".tr, + //Constant.amountShow(amount: controller.price.value.toString()), + controller.onProviderOrder.value?.provider.disPrice == "" || controller.onProviderOrder.value?.provider.disPrice == "0" + ? "${Constant.amountShow(amount: controller.onProviderOrder.value?.provider.price.toString())} × ${controller.onProviderOrder.value?.quantity.toStringAsFixed(2)} ${Constant.amountShow(amount: controller.price.value.toString())}" + : "${Constant.amountShow(amount: controller.onProviderOrder.value?.provider.disPrice.toString())} × ${controller.onProviderOrder.value?.quantity.toStringAsFixed(2)} ${Constant.amountShow(amount: controller.price.value.toString())}", + isDark, + ), + controller.discountAmount.value != 0 ? const Divider() : const SizedBox(), + controller.discountAmount.value != 0 + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${"Discount".tr} ${controller.discountType.value == 'Percentage' || controller.discountType.value == 'Percent' ? "(${controller.discountLabel.value}%)" : "(${Constant.amountShow(amount: controller.discountLabel.value)})"}", + style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Text(controller.offerCode.value, style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ), + Text("(-${Constant.amountShow(amount: controller.discountAmount.value.toString())})", style: const TextStyle(color: Colors.red)), + ], + ), + ) + : const SizedBox(), + const Divider(), + rowText("SubTotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark), + const Divider(), + ListView.builder( + itemCount: Constant.taxList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + TaxModel taxModel = Constant.taxList[index]; + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "${taxModel.title} (${taxModel.type == "fix" ? Constant.amountShow(amount: taxModel.tax) : "${taxModel.tax}%"})", + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + Constant.amountShow(amount: Constant.getTaxValue(amount: controller.subTotal.value.toString(), taxModel: taxModel).toString()), + style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + const Divider(), + ], + ); + }, + ), + // Total Amount + rowText("Total Amount".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark), + const SizedBox(height: 5), + ], + ), + ); + }); + } + + Widget rowText(String title, String value, bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(value, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ); + } + + Future showCancelBookingDialog(OnDemandOrderDetailsController controller, bool isDark) { + return Get.dialog( + AlertDialog( + backgroundColor: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + title: Text('Please give reason for canceling this Booking'.tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + content: TextFormField( + controller: controller.cancelBookingController.value, + maxLines: 5, + decoration: InputDecoration(hintText: "Specify your reason here".tr, border: OutlineInputBorder(borderRadius: BorderRadius.circular(7))), + ), + actions: [ + TextButton(onPressed: () => Get.back(), child: Text('Cancel'.tr, style: TextStyle(color: Colors.red))), + TextButton( + onPressed: () async { + if (controller.cancelBookingController.value.text.trim().isEmpty) { + ShowToastDialog.showToast("Please enter reason".tr); + } else { + await controller.cancelBooking(); + } + }, + child: Text('Continue'.tr, style: TextStyle(color: Colors.green)), + ), + ], + ), + barrierDismissible: false, + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_payment_screen.dart b/lib/screen_ui/on_demand_service/on_demand_payment_screen.dart new file mode 100644 index 0000000..1ea95bd --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_payment_screen.dart @@ -0,0 +1,263 @@ +import 'package:customer/constant/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/0n_demand_payment_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../payment/createRazorPayOrderModel.dart'; +import '../../payment/rozorpayConroller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../multi_vendor_service/wallet_screen/wallet_screen.dart'; + +class OnDemandPaymentScreen extends StatelessWidget { + const OnDemandPaymentScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: OnDemandPaymentController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Select Payment Method".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark200 : Colors.white), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + children: [ + const SizedBox(height: 10), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + const SizedBox(height: 10), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility( + visible: controller.payStackModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png"), + ), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility( + visible: controller.razorPayModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"), + ), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + SizedBox(height: 20), + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + onPress: () async { + print("getTotalAmount :::::::: ${"${controller.totalAmount.value}"}"); + if (controller.isOrderPlaced.value == false) { + controller.isOrderPlaced.value = true; + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet("${controller.totalAmount.value}", context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment("${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + double totalAmount = double.parse("${controller.totalAmount.value}"); + double walletAmount = double.tryParse(Constant.userModel?.walletAmount?.toString() ?? "0") ?? 0; + + if (walletAmount == 0) { + ShowToastDialog.showToast("Wallet balance is 0. Please recharge wallet.".tr); + } else if (walletAmount < totalAmount) { + ShowToastDialog.showToast("Insufficient wallet balance. Please add funds.".tr); + } else { + controller.placeOrder(); + } + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.placeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + controller.placeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, "${controller.totalAmount.value}"); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse("${controller.totalAmount.value}"), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: "${controller.totalAmount.value}", orderId: result.id); + } + }); + } else { + controller.isOrderPlaced.value = false; + ShowToastDialog.showToast("Please select payment method".tr); + } + controller.isOrderPlaced.value = false; + } + }, + ), + ], + ), + ), + ), + ); + }, + ); + } + + Obx cardDecoration(controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: Constant.userModel?.walletAmount == null ? '0.0' : Constant.userModel?.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/on_demand_review_screen.dart b/lib/screen_ui/on_demand_service/on_demand_review_screen.dart new file mode 100644 index 0000000..a147698 --- /dev/null +++ b/lib/screen_ui/on_demand_service/on_demand_review_screen.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/on_demand_review_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import '../../utils/network_image_widget.dart'; + +class OnDemandReviewScreen extends StatelessWidget { + const OnDemandReviewScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetBuilder( + init: OnDemandReviewController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text(controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: Obx( + () => + (controller.reviewFor.value == "Worker" && controller.workerModel.value == null) || (controller.reviewFor.value == "Provider" && controller.provider.value == null) + ? Constant.loader() + : Padding( + padding: const EdgeInsets.only(top: 50.0), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 42, bottom: 20), + child: Card( + elevation: 2, + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 65), + child: Column( + children: [ + Text('Rate for'.tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text( + controller.reviewFor.value == "Provider" ? controller.order.value!.provider.authorName ?? "" : controller.workerModel.value!.fullName(), + style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: RatingBar.builder( + initialRating: controller.ratings.value, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemPadding: const EdgeInsets.symmetric(horizontal: 4.0), + itemBuilder: (context, _) => const Icon(Icons.star, color: Colors.amber), + unratedColor: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400, + onRatingUpdate: (rating) { + controller.ratings.value = rating; + }, + ), + ), + Padding(padding: EdgeInsets.all(20.0), child: TextFieldWidget(hintText: "Type comment....".tr, controller: controller.comment, maxLine: 5)), + Padding( + padding: const EdgeInsets.all(20.0), + child: RoundedButtonFill( + title: controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: controller.submitReview, + ), + ), + ], + ), + ), + ), + ), + ), + Align( + alignment: Alignment.topCenter, + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: NetworkImageWidget( + imageUrl: controller.reviewFor.value == "Provider" ? controller.order.value?.provider.authorProfilePic ?? '' : controller.workerModel.value?.profilePictureURL ?? '', + fit: BoxFit.cover, + height: 100, + width: 100, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/on_demand_service/provider_inbox_screen.dart b/lib/screen_ui/on_demand_service/provider_inbox_screen.dart new file mode 100644 index 0000000..617c526 --- /dev/null +++ b/lib/screen_ui/on_demand_service/provider_inbox_screen.dart @@ -0,0 +1,127 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/inbox_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/widget/firebase_pagination/src/fireStore_pagination.dart'; +import 'package:customer/widget/firebase_pagination/src/models/view_type.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; + +class ProviderInboxScreen extends StatelessWidget { + const ProviderInboxScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Provider Inbox".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: FirestorePagination( + //item builder type is compulsory. + physics: const BouncingScrollPhysics(), + itemBuilder: (context, documentSnapshots, index) { + final data = documentSnapshots[index].data() as Map?; + InboxModel inboxModel = InboxModel.fromJson(data!); + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(inboxModel.customerId.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(inboxModel.restaurantId.toString()); + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": restaurantUser!.fullName(), + "orderId": inboxModel.orderId, + "restaurantId": restaurantUser.id, + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": restaurantUser.profilePictureURL, + "token": restaurantUser.fcmToken, + "chatType": inboxModel.chatType, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: NetworkImageWidget( + imageUrl: inboxModel.restaurantProfileImage.toString(), + fit: BoxFit.cover, + height: Responsive.height(6, context), + width: Responsive.width(12, context), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "${inboxModel.restaurantName}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ), + Text( + Constant.timestampToDate(inboxModel.createdAt!), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + const SizedBox(height: 5), + Text( + "${inboxModel.lastMessage}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + shrinkWrap: true, + onEmpty: Constant.showEmptyView(message: "No Conversion found".tr), + // orderBy is compulsory to enable pagination + query: FirebaseFirestore.instance.collection('chat_provider').where("customerId", isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true), + //Change types customerId + viewType: ViewType.list, + initialLoader: Constant.loader(), + // to fetch real-time data + isLive: true, + ), + ); + } +} diff --git a/lib/screen_ui/on_demand_service/provider_screen.dart b/lib/screen_ui/on_demand_service/provider_screen.dart new file mode 100644 index 0000000..9290009 --- /dev/null +++ b/lib/screen_ui/on_demand_service/provider_screen.dart @@ -0,0 +1,116 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/screen_ui/on_demand_service/on_demand_home_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../controllers/provider_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/provider_serivce_model.dart'; +import '../../themes/app_them_data.dart'; + +class ProviderScreen extends StatelessWidget { + const ProviderScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: ProviderController(), + builder: (controller) { + return Scaffold( + appBar: AppBar(automaticallyImplyLeading: true), + body: + controller.isLoading.value + ? Center(child: Constant.loader()) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 50), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: + (controller.userModel.value?.profilePictureURL ?? "").isNotEmpty + ? CircleAvatar(backgroundImage: NetworkImage(controller.userModel.value?.profilePictureURL ?? ''), radius: 50.0) + : CircleAvatar(backgroundImage: NetworkImage(Constant.placeHolderImage), radius: 50.0), + ), + const SizedBox(height: 10), + Text( + controller.userModel.value?.fullName() ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 20, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_mail.svg", color: isDark ? Colors.white : Colors.black), + const SizedBox(width: 6), + Text( + controller.userModel.value?.email ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.w500), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_mobile.svg", color: isDark ? Colors.white : Colors.black), + const SizedBox(width: 6), + Text( + controller.userModel.value?.phoneNumber ?? '', + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.w500), + ), + ], + ), + const SizedBox(height: 10), + Container( + decoration: const BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.all(Radius.circular(16))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star, size: 16, color: Colors.white), + const SizedBox(width: 3), + Text( + _getRating(controller), + style: const TextStyle(letterSpacing: 0.5, fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: Colors.white), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + controller.providerList.isEmpty + ? Center(child: Text("No Services Found".tr)) + : Expanded( + child: ListView.builder( + itemCount: controller.providerList.length, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + ProviderServiceModel data = controller.providerList[index]; + return ServiceView(provider: data, isDark: isDark, controller: controller.onDemandHomeController.value); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + + String _getRating(ProviderController controller) { + final reviewsCount = double.tryParse(controller.userModel.value?.reviewsCount?.toString() ?? "0") ?? 0; + final reviewsSum = double.tryParse(controller.userModel.value?.reviewsSum?.toString() ?? "0") ?? 0; + + if (reviewsCount == 0) return "0"; + final avg = reviewsSum / reviewsCount; + return avg.toStringAsFixed(1); + } +} diff --git a/lib/screen_ui/on_demand_service/view_all_popular_service_screen.dart b/lib/screen_ui/on_demand_service/view_all_popular_service_screen.dart new file mode 100644 index 0000000..67a35cc --- /dev/null +++ b/lib/screen_ui/on_demand_service/view_all_popular_service_screen.dart @@ -0,0 +1,74 @@ +import 'package:customer/screen_ui/on_demand_service/on_demand_home_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/theme_controller.dart'; +import '../../controllers/view_all_popular_service_controller.dart'; +import '../../models/provider_serivce_model.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/text_field_widget.dart'; + +class ViewAllPopularServiceScreen extends StatelessWidget { + const ViewAllPopularServiceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ViewAllPopularServiceController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("All Services".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), + child: Column( + children: [ + TextFieldWidget(hintText: "Search Service".tr, controller: controller.searchTextFiledController.value, onchange: (value) => controller.getFilterData(value.toString())), + const SizedBox(height: 15), + controller.providerList.isEmpty + ? Expanded(child: Center(child: Constant.showEmptyView(message: "No service Found".tr))) + : Expanded( + child: ListView.builder( + itemCount: controller.providerList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + ProviderServiceModel data = controller.providerList[index]; + return ServiceView(provider: data, isDark: isDark, controller: controller.onDemandHomeController.value); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/on_demand_service/view_category_service_screen.dart b/lib/screen_ui/on_demand_service/view_category_service_screen.dart new file mode 100644 index 0000000..ae5be3d --- /dev/null +++ b/lib/screen_ui/on_demand_service/view_category_service_screen.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/theme_controller.dart'; +import '../../controllers/view_category_service_controller.dart'; +import '../../models/provider_serivce_model.dart'; +import '../../screen_ui/on_demand_service/on_demand_home_screen.dart'; +import '../../themes/app_them_data.dart'; + +class ViewCategoryServiceListScreen extends StatelessWidget { + const ViewCategoryServiceListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: ViewCategoryServiceController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text(controller.categoryTitle.value, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : controller.providerList.isEmpty + ? Constant.showEmptyView(message: "No Service Found".tr) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: ListView.builder( + itemCount: controller.providerList.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + ProviderServiceModel providerModel = controller.providerList[index]; + return ServiceView(isDark: isDark, provider: providerModel, controller: controller.onDemandHomeController.value); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/on_demand_service/worker_inbox_screen.dart b/lib/screen_ui/on_demand_service/worker_inbox_screen.dart new file mode 100644 index 0000000..acbe6ca --- /dev/null +++ b/lib/screen_ui/on_demand_service/worker_inbox_screen.dart @@ -0,0 +1,128 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/inbox_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:customer/widget/firebase_pagination/src/fireStore_pagination.dart'; +import 'package:customer/widget/firebase_pagination/src/models/view_type.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../controllers/theme_controller.dart'; +import '../../../service/fire_store_utils.dart'; +import '../../../themes/show_toast_dialog.dart'; + +class WorkerInboxScreen extends StatelessWidget { + const WorkerInboxScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("Worker Inbox".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: FirestorePagination( + //item builder type is compulsory. + physics: const BouncingScrollPhysics(), + itemBuilder: (context, documentSnapshots, index) { + final data = documentSnapshots[index].data() as Map?; + InboxModel inboxModel = InboxModel.fromJson(data!); + return InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(inboxModel.customerId.toString()); + UserModel? restaurantUser = await FireStoreUtils.getUserProfile(inboxModel.restaurantId.toString()); + ShowToastDialog.closeLoader(); + + print("customerId: ${inboxModel.customerId}, restaurantId: ${inboxModel.restaurantId}"); + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer!.fullName(), + "restaurantName": restaurantUser?.fullName() ?? '', + "orderId": inboxModel.orderId, + "restaurantId": restaurantUser?.id ?? '', + "customerId": customer.id, + "customerProfileImage": customer.profilePictureURL, + "restaurantProfileImage": restaurantUser?.profilePictureURL, + "token": restaurantUser?.fcmToken, + "chatType": inboxModel.chatType, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + child: Container( + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: NetworkImageWidget( + imageUrl: inboxModel.restaurantProfileImage.toString(), + fit: BoxFit.cover, + height: Responsive.height(6, context), + width: Responsive.width(12, context), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + "${inboxModel.restaurantName}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800), + ), + ), + Text( + Constant.timestampToDate(inboxModel.createdAt!), + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.regular, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ], + ), + const SizedBox(height: 5), + Text( + "${inboxModel.lastMessage}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + shrinkWrap: true, + onEmpty: Constant.showEmptyView(message: "No Conversion found".tr), + // orderBy is compulsory to enable pagination + query: FirebaseFirestore.instance.collection('chat_worker').where("customerId", isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true), + //Change types customerId + viewType: ViewType.list, + initialLoader: Constant.loader(), + // to fetch real-time data + isLive: true, + ), + ); + } +} diff --git a/lib/screen_ui/parcel_service/book_parcel_screen.dart b/lib/screen_ui/parcel_service/book_parcel_screen.dart new file mode 100644 index 0000000..084b6b7 --- /dev/null +++ b/lib/screen_ui/parcel_service/book_parcel_screen.dart @@ -0,0 +1,476 @@ +import 'dart:io'; +import 'package:country_code_picker/country_code_picker.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/utils.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:dropdown_textfield/dropdown_textfield.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/book_parcel_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/user_model.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import '../../widget/osm_map/map_picker_page.dart'; +import '../../widget/place_picker/location_picker_screen.dart'; +import '../../widget/place_picker/selected_location_model.dart'; + +class BookParcelScreen extends StatelessWidget { + const BookParcelScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: BookParcelController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Book Your Document Delivery".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + Text( + "Schedule a secure and timely pickup & delivery".tr, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppThemeData.mediumTextStyle(fontSize: 12, color: AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + selectDeliveryTypeView(controller, isDark, context), + + const SizedBox(height: 16), + + buildUploadBoxView(isDark, controller), + + const SizedBox(height: 16), + buildInfoSectionView( + title: "Sender Information".tr, + locationController: controller.senderLocationController.value, + nameController: controller.senderNameController.value, + mobileController: controller.senderMobileController.value, + noteController: controller.senderNoteController.value, + countryCodeController: controller.senderCountryCodeController.value, + showWeight: true, + isDark: isDark, + context: context, + controller: controller, + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + + if (Constant.checkZoneCheck(firstPlace.coordinates.latitude, firstPlace.coordinates.longitude) == true) { + final address = firstPlace.address; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + controller.senderLocationController.value.text = address; // ✅ + controller.senderLocation.value = UserLocation(latitude: lat, longitude: lng); // ✅ <-- Add this + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + if (Constant.checkZoneCheck(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude) == true) { + controller.senderLocationController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.senderLocation.value = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + // ✅ <-- Add this + } + }); + } + }, + ), + const SizedBox(height: 16), + buildInfoSectionView( + title: "Receiver Information".tr, + locationController: controller.receiverLocationController.value, + nameController: controller.receiverNameController.value, + mobileController: controller.receiverMobileController.value, + noteController: controller.receiverNoteController.value, + countryCodeController: controller.receiverCountryCodeController.value, + showWeight: false, + isDark: isDark, + context: context, + controller: controller, + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + + if (Constant.checkZoneCheck(firstPlace.coordinates.latitude, firstPlace.coordinates.longitude) == true) { + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + controller.receiverLocationController.value.text = address; // ✅ + controller.receiverLocation.value = UserLocation(latitude: lat, longitude: lng); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + if (Constant.checkZoneCheck(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude) == true) { + controller.receiverLocationController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.receiverLocation.value = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); // ✅ <-- Add this + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + }); + } + }, + ), + + const SizedBox(height: 15), + + RoundedButtonFill( + title: "Continue".tr, + onPress: () { + controller.bookNow(); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + const SizedBox(height: 25), + ], + ), + ), + ); + }, + ); + } + + Widget selectDeliveryTypeView(BookParcelController controller, bool isDark, BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Select delivery type".tr, style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500, fontSize: 13)), + const SizedBox(height: 10), + InkWell( + onTap: () { + controller.selectedDeliveryType.value = 'now'; + controller.isScheduled.value = false; + }, + child: Row( + children: [ + Image.asset("assets/images/image_parcel.png", height: 38, width: 38), + const SizedBox(width: 20), + Expanded(child: Text("As soon as possible".tr, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16))), + Icon( + controller.selectedDeliveryType.value == 'now' ? Icons.radio_button_checked : Icons.radio_button_off, + color: controller.selectedDeliveryType.value == 'now' ? AppThemeData.primary300 : (isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + size: 20, + ), + ], + ), + ), + const SizedBox(height: 10), + InkWell( + onTap: () { + controller.selectedDeliveryType.value = 'later'; + controller.isScheduled.value = true; + }, + child: Column( + children: [ + Row( + children: [ + Image.asset("assets/images/image_parcel_scheduled.png", height: 38, width: 38), + const SizedBox(width: 20), + Expanded(child: Text("Scheduled".tr, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16))), + Icon( + controller.selectedDeliveryType.value == 'later' ? Icons.radio_button_checked : Icons.radio_button_off, + color: controller.selectedDeliveryType.value == 'later' ? AppThemeData.primary300 : (isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + size: 20, + ), + ], + ), + if (controller.selectedDeliveryType.value == 'later') ...[ + const SizedBox(height: 10), + GestureDetector( + onTap: () => controller.pickScheduledDate(context), + child: TextFieldWidget( + hintText: "When to pickup at this address".tr, + controller: controller.scheduledDateController.value, + enable: false, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + suffix: const Padding(padding: EdgeInsets.only(right: 10), child: Icon(Icons.calendar_month_outlined)), + ), + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () => controller.pickScheduledTime(context), + child: TextFieldWidget( + hintText: "When to pickup at this address".tr, + controller: controller.scheduledTimeController.value, + enable: false, + // onchange: (v) => controller.pickScheduledTime(context), + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + suffix: const Padding(padding: EdgeInsets.only(right: 10), child: Icon(Icons.access_time)), + ), + ), + ], + ], + ), + ), + ], + ), + ); + } + + Widget buildUploadBoxView(bool isDark, BookParcelController controller) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Upload parcel image".tr, style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500, fontSize: 13)), + const SizedBox(height: 10), + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(10), color: isDark ? AppThemeData.greyDark300 : AppThemeData.grey300), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icons/ic_upload_parcel.svg", height: 40, width: 40), + const SizedBox(height: 10), + Text("Upload Parcel Image".tr, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 4), + Text("Supported: .jpg, .jpeg, .png".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text("Max size 1MB".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + const SizedBox(height: 8), + RoundedButtonFill( + title: "Browse Image".tr, + onPress: () { + controller.onCameraClick(Get.context!); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + width: 40, + ), + ], + ), + ), + ), + const SizedBox(height: 10), + if (controller.images.isEmpty) const SizedBox(), + Wrap( + spacing: 10, + runSpacing: 10, + children: + controller.images.map((image) { + return Stack( + children: [ + Container( + padding: const EdgeInsets.only(top: 20, right: 20), + child: ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.file(File(image.path), width: 70, height: 70, fit: BoxFit.cover)), + ), + Positioned.fill( + top: 0, + right: 0, + child: Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.cancel, color: AppThemeData.danger300, size: 20), + onPressed: () { + controller.images.remove(image); + }, + ), + ), + ), + ], + ); + }).toList(), + ), + ], + ), + ); + } + + Widget buildInfoSectionView({ + required String title, + required TextEditingController locationController, + required TextEditingController nameController, + required TextEditingController mobileController, + required TextEditingController noteController, + required TextEditingController countryCodeController, + bool showWeight = false, + GestureTapCallback? onTap, + required bool isDark, + required BookParcelController controller, + required BuildContext context, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500, fontSize: 13)), + const SizedBox(height: 10), + + GestureDetector( + onTap: onTap, + child: TextFieldWidget( + hintText: "Your Location".tr, + controller: locationController, + + suffix: const Padding(padding: EdgeInsets.only(right: 10), child: Icon(Icons.location_on_outlined)), + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + enable: false, + ), + ), + const SizedBox(height: 10), + + TextFieldWidget( + hintText: "Name".tr, + controller: nameController, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + ), + const SizedBox(height: 10), + + TextFieldWidget( + hintText: "Enter Mobile number".tr, + controller: mobileController, + textInputType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9]')), LengthLimitingTextInputFormatter(10)], + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + prefix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CountryCodePicker( + onChanged: (value) { + countryCodeController.text = value.dialCode ?? Constant.defaultCountryCode; + }, + initialSelection: countryCodeController.text.isNotEmpty ? countryCodeController.text : Constant.defaultCountryCode, + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + textStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : Colors.black), + dialogTextStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + searchStyle: TextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + dialogBackgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + padding: EdgeInsets.zero, + ), + // const Icon(Icons.keyboard_arrow_down_rounded, size: 24, color: AppThemeData.grey400), + Container(height: 24, width: 1, color: AppThemeData.grey400), + const SizedBox(width: 4), + ], + ), + ), + + if (showWeight) ...[ + const SizedBox(height: 10), + DropDownTextField( + controller: controller.senderWeightController.value, + clearOption: false, + enableSearch: false, + textFieldDecoration: InputDecoration( + hintText: "Select parcel Weight".tr, + hintStyle: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.grey400 : AppThemeData.greyDark400), + filled: true, + fillColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppThemeData.grey200)), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppThemeData.grey200)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppThemeData.grey200)), + ), + dropDownList: + controller.parcelWeight.map((e) { + return DropDownValueModel( + name: e.title ?? 'Normal'.tr, + value: e.title ?? 'Normal'.tr, // safer to use title string + ); + }).toList(), + onChanged: (val) { + if (val is DropDownValueModel) { + controller.senderWeightController.value.setDropDown(val); + + // Link it to the selectedWeight object + controller.selectedWeight = controller.parcelWeight.firstWhereOrNull((e) => e.title == val.value); + } + }, + ), + ], + + const SizedBox(height: 10), + TextFieldWidget( + hintText: "Notes (Optional)".tr, + controller: noteController, + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + borderColor: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200, + ), + ], + ), + ); + } +} diff --git a/lib/screen_ui/parcel_service/home_parcel_screen.dart b/lib/screen_ui/parcel_service/home_parcel_screen.dart new file mode 100644 index 0000000..dd91551 --- /dev/null +++ b/lib/screen_ui/parcel_service/home_parcel_screen.dart @@ -0,0 +1,275 @@ +import 'package:customer/constant/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import '../../controllers/home_parcel_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/banner_model.dart'; +import '../../models/parcel_category.dart'; +import '../../models/user_model.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../utils/network_image_widget.dart'; +import '../../widget/osm_map/map_picker_page.dart'; +import '../../widget/place_picker/location_picker_screen.dart'; +import '../../widget/place_picker/selected_location_model.dart'; +import '../auth_screens/login_screen.dart'; +import '../location_enable_screens/address_list_screen.dart'; +import 'book_parcel_screen.dart'; + +class HomeParcelScreen extends StatelessWidget { + const HomeParcelScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: HomeParcelController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () { + Get.back(); + }, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell( + onTap: () { + Get.offAll(const LoginScreen()); + }, + child: Text("Login".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900)), + ) + : Text(Constant.userModel!.fullName(), style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900)), + InkWell( + onTap: () async { + if (Constant.userModel != null) { + Get.to(AddressListScreen())!.then((value) { + if (value != null) { + ShippingAddress shippingAddress = value; + Constant.selectedLocation = shippingAddress; + } + }); + } else { + Constant.checkPermission( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + ShippingAddress shippingAddress = ShippingAddress(); + + try { + await Geolocator.requestPermission(); + await Geolocator.getCurrentPosition(); + ShowToastDialog.closeLoader(); + + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + final address = firstPlace.address; + + shippingAddress.addressAs = "Home"; + shippingAddress.locality = address.toString(); + shippingAddress.location = UserLocation(latitude: lat, longitude: lng); + + Constant.selectedLocation = shippingAddress; + Get.back(); + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + shippingAddress.addressAs = "Home"; + shippingAddress.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude); + shippingAddress.locality = "Picked from Map"; + + Constant.selectedLocation = shippingAddress; + } + }); + } + } catch (e) { + await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) { + Placemark placeMark = valuePlaceMaker[0]; + shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118); + String currentLocation = + "${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}"; + shippingAddress.locality = currentLocation; + }); + + Constant.selectedLocation = shippingAddress; + ShowToastDialog.closeLoader(); + } + }, + context: context, + ); + } + }, + child: Text( + Constant.selectedLocation.getFullAddress(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900), + ), + ), + ], + ), + ), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Center(child: Constant.loader()) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + BannerView(bannerList: controller.bannerTopHome), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("What are you sending?".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ListView.builder( + itemCount: controller.parcelCategory.length, + shrinkWrap: true, + physics: const ScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 10), + itemBuilder: (context, index) { + return buildItems(item: controller.parcelCategory[index], isDark: isDark); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget buildItems({required ParcelCategory item, required bool isDark}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: InkWell( + onTap: () { + if (Constant.userModel == null) { + Get.to(const LoginScreen()); + } else { + Get.to(const BookParcelScreen(), arguments: {'parcelCategory': item}); + } + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + NetworkImageWidget(imageUrl: item.image ?? '', height: 38, width: 38), + const SizedBox(width: 20), + Expanded(child: Text(item.title ?? '', style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16))), + Icon(Icons.arrow_forward_ios, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800, size: 20), + ], + ), + ), + ); + } +} + +class BannerView extends StatelessWidget { + final List bannerList; + final RxInt currentPage = 0.obs; + final ScrollController scrollController = ScrollController(); + + BannerView({super.key, required this.bannerList}); + + /// Computes the visible item index from scroll offset + void onScroll(BuildContext context) { + if (scrollController.hasClients && bannerList.isNotEmpty) { + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = screenWidth * 0.8 + 10; // banner width + spacing + final offset = scrollController.offset; + final index = (offset / itemWidth).round(); + + if (index != currentPage.value && index < bannerList.length) { + currentPage.value = index; + } + } + } + + @override + Widget build(BuildContext context) { + scrollController.addListener(() { + onScroll(context); + }); + + return Column( + children: [ + SizedBox( + height: 150, + child: ListView.separated( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: bannerList.length, + separatorBuilder: (context, index) => const SizedBox(width: 15), + itemBuilder: (context, index) { + final banner = bannerList[index]; + return ClipRRect( + borderRadius: BorderRadius.circular(15), + child: SizedBox(width: MediaQuery.of(context).size.width * 0.8, child: NetworkImageWidget(imageUrl: banner.photo ?? '', fit: BoxFit.cover)), + ); + }, + ), + ), + const SizedBox(height: 8), + Obx(() { + return Row( + children: List.generate(bannerList.length, (index) { + bool isSelected = currentPage.value == index; + return Expanded(child: Container(height: 4, decoration: BoxDecoration(color: isSelected ? AppThemeData.grey300 : AppThemeData.grey100, borderRadius: BorderRadius.circular(5)))); + }), + ); + }), + ], + ); + } +} diff --git a/lib/screen_ui/parcel_service/my_booking_screen.dart b/lib/screen_ui/parcel_service/my_booking_screen.dart new file mode 100644 index 0000000..2b6a99b --- /dev/null +++ b/lib/screen_ui/parcel_service/my_booking_screen.dart @@ -0,0 +1,209 @@ +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/screen_ui/parcel_service/parcel_order_details.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/parcel_my_booking_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import 'package:dotted_border/dotted_border.dart'; + +class MyBookingScreen extends StatelessWidget { + const MyBookingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: ParcelMyBookingController(), + builder: (controller) { + return DefaultTabController( + length: controller.tabTitles.length, + initialIndex: controller.tabTitles.indexOf(controller.selectedTab.value), + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row(children: [const SizedBox(width: 10), Text("Parcel History".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900))]), + ), + bottom: TabBar( + // don't re-subscribe onTap — just update selectedTab (optional) + onTap: (index) { + controller.selectTab(controller.tabTitles[index]); + }, + indicatorColor: AppThemeData.parcelService500, + labelColor: AppThemeData.parcelService500, + unselectedLabelColor: AppThemeData.parcelService500, + labelStyle: AppThemeData.boldTextStyle(fontSize: 15), + unselectedLabelStyle: AppThemeData.mediumTextStyle(fontSize: 15), + tabs: controller.tabTitles.map((title) => Tab(child: Center(child: Text(title)))).toList(), + ), + ), + + body: + controller.isLoading.value + ? Constant.loader() + : Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : TabBarView( + children: + controller.tabTitles.map((title) { + final orders = controller.getOrdersForTab(title); + + if (orders.isEmpty) { + return Center(child: Text("No orders found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: orders.length, + itemBuilder: (context, index) { + final order = orders[index]; + return GestureDetector( + onTap: () { + Get.to(() => const ParcelOrderDetails(), arguments: order); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${'Order Date:'.tr}${order.isSchedule == true ? controller.formatDate(order.createdAt!) : controller.formatDate(order.senderPickupDateTime!)}", + style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.info400), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Image.asset("assets/images/image_parcel.png", height: 32, width: 32), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 95), + ), + Image.asset("assets/images/image_parcel.png", height: 32, width: 32), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _infoSection( + "Pickup Address (Sender):".tr, + order.sender?.name ?? '', + order.sender?.address ?? '', + order.sender?.phone ?? '', + // order.senderPickupDateTime != null + // ? "Pickup Time: ${controller.formatDate(order.senderPickupDateTime!)}" + // : '', + order.status, + isDark, + ), + const SizedBox(height: 16), + _infoSection( + "Delivery Address (Receiver):".tr, + order.receiver?.name ?? '', + order.receiver?.address ?? '', + order.receiver?.phone ?? '', + // order.receiverPickupDateTime != null + // ? "Delivery Time: ${controller.formatDate(order.receiverPickupDateTime!)}" + // : '', + null, + isDark, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }).toList(), + ), + ), + ); + }, + ); + } + + Widget _infoSection(String title, String name, String address, String phone, String? status, bool isDark) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(title, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), maxLines: 1, overflow: TextOverflow.ellipsis), + ), + if (status != null) ...[ + Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + decoration: BoxDecoration(color: AppThemeData.info50, border: Border.all(color: AppThemeData.info300), borderRadius: BorderRadius.circular(12)), + child: Text(status, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.info500)), + ), + ], + ], + ), + Text(name, style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(address, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(phone, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + //Text(time, style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ); + } +} diff --git a/lib/screen_ui/parcel_service/order_successfully_placed.dart b/lib/screen_ui/parcel_service/order_successfully_placed.dart new file mode 100644 index 0000000..9379cf1 --- /dev/null +++ b/lib/screen_ui/parcel_service/order_successfully_placed.dart @@ -0,0 +1,65 @@ +import 'package:customer/screen_ui/parcel_service/parcel_dashboard_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import '../../controllers/parcel_dashboard_controller.dart'; +import '../../controllers/theme_controller.dart'; + +class OrderSuccessfullyPlaced extends StatelessWidget { + const OrderSuccessfullyPlaced({super.key}); + + @override + Widget build(BuildContext context) { + final parcelOrder = Get.arguments['parcelOrder']; + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("assets/images/parcel_order_successfully_placed.png"), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Text( + "Your Order Has Been Placed!".tr, + style: AppThemeData.boldTextStyle(fontSize: 22, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 50), + child: Text( + "We’ve received your parcel booking and it’s now being processed. You can track its status in real time.".tr, + style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 30), + RoundedButtonFill( + title: "Track Your Order".tr, + onPress: () { + print("Tracking Order: $parcelOrder"); + //Get.to(() => TrackOrderScreen(), arguments: {'order': parcelOrder}); + Get.offAll(const ParcelDashboardScreen()); + ParcelDashboardController controller = Get.put(ParcelDashboardController()); + controller.selectedIndex.value = 1; + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/parcel_service/parcel_coupon_screen.dart b/lib/screen_ui/parcel_service/parcel_coupon_screen.dart new file mode 100644 index 0000000..6de1f4c --- /dev/null +++ b/lib/screen_ui/parcel_service/parcel_coupon_screen.dart @@ -0,0 +1,138 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/parcel_coupon_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/widget/my_separator.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ParcelCouponScreen extends StatelessWidget { + const ParcelCouponScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ParcelCouponController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Coupon".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : controller.cabCouponList.isEmpty + ? Constant.showEmptyView(message: "Coupon not found".tr) + : ListView.builder( + shrinkWrap: true, + itemCount: controller.cabCouponList.length, + itemBuilder: (context, index) { + CouponModel couponModel = controller.cabCouponList[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Container( + height: Responsive.height(16, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + child: Stack( + children: [ + Image.asset("assets/images/ic_coupon_image.png", height: Responsive.height(16, context), fit: BoxFit.fill), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Align( + alignment: Alignment.center, + child: RotatedBox( + quarterTurns: -1, + child: Text( + "${couponModel.discountType == "Fix Price" ? Constant.amountShow(amount: couponModel.discount) : "${couponModel.discount}%"} ${'Off'.tr}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(6), color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "${couponModel.code}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + const Expanded(child: SizedBox(height: 10)), + InkWell( + onTap: () { + Get.back(result: couponModel); + }, + child: Text( + "Tap To Apply".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 20), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 20), + Text( + "${couponModel.description}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/parcel_service/parcel_dashboard_screen.dart b/lib/screen_ui/parcel_service/parcel_dashboard_screen.dart new file mode 100644 index 0000000..9058bc3 --- /dev/null +++ b/lib/screen_ui/parcel_service/parcel_dashboard_screen.dart @@ -0,0 +1,80 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/parcel_dashboard_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +class ParcelDashboardScreen extends StatelessWidget { + const ParcelDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: ParcelDashboardController(), + builder: (controller) { + return Scaffold( + body: controller.pageList[controller.selectedIndex.value], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + showUnselectedLabels: true, + showSelectedLabels: true, + selectedFontSize: 12, + selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + currentIndex: controller.selectedIndex.value, + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + selectedItemColor: isDark ? AppThemeData.parcelServiceDark300 : AppThemeData.primary300, + unselectedItemColor: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500, + onTap: (int index) { + if (index == 0) { + Get.put(ParcelDashboardController()); + } + controller.selectedIndex.value = index; + }, + items: + Constant.walletSetting == false + ? [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_parcel.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_mybooking_parcel.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_profile_parcel.svg", label: 'Profile'.tr, controller: controller), + ] + : [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_parcel.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_mybooking_parcel.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_wallet_parcel.svg", label: 'Wallet'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile_parcel.svg", label: 'Profile'.tr, controller: controller), + ], + ), + ); + }, + ); + }); + } + + BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required ParcelDashboardController controller}) { + return BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: SvgPicture.asset( + assetIcon, + height: label == 'Wallet'.tr ? 18 : 22, + width: label == 'Wallet'.tr ? 18 : 22, + color: + controller.selectedIndex.value == index + ? isDark + ? AppThemeData.parcelServiceDark300 + : AppThemeData.primary300 + : isDark + ? AppThemeData.grey300 + : AppThemeData.grey600, + ), + ), + label: label, + ); + } +} diff --git a/lib/screen_ui/parcel_service/parcel_order_confirmation.dart b/lib/screen_ui/parcel_service/parcel_order_confirmation.dart new file mode 100644 index 0000000..6c54262 --- /dev/null +++ b/lib/screen_ui/parcel_service/parcel_order_confirmation.dart @@ -0,0 +1,591 @@ +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/screen_ui/parcel_service/parcel_coupon_screen.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/parcel_order_confirmation_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../payment/createRazorPayOrderModel.dart'; +import '../../payment/rozorpayConroller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../multi_vendor_service/wallet_screen/wallet_screen.dart'; + +class ParcelOrderConfirmationScreen extends StatelessWidget { + const ParcelOrderConfirmationScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ParcelOrderConfirmationController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Order Confirmation".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Pickup and Delivery Info + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Timeline with icons and line + Column( + children: [ + Image.asset("assets/images/image_parcel.png", height: 32, width: 32), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 95), + ), + Image.asset("assets/images/image_parcel.png", height: 32, width: 32), + ], + ), + const SizedBox(width: 12), + // Address Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _infoSection( + "Pickup Address (Sender):".tr, + controller.parcelOrder.value.sender?.name ?? '', + controller.parcelOrder.value.sender?.address ?? '', + controller.parcelOrder.value.sender?.phone ?? '', + // controller.parcelOrder.value.senderPickupDateTime != null + // ? "Pickup Time: ${controller.formatDate(controller.parcelOrder.value.senderPickupDateTime!)}" + // : '', + isDark, + ), + const SizedBox(height: 16), + _infoSection( + "Delivery Address (Receiver):".tr, + controller.parcelOrder.value.receiver?.name ?? '', + controller.parcelOrder.value.receiver?.address ?? '', + controller.parcelOrder.value.receiver?.phone ?? '', + // controller.parcelOrder.value.receiverPickupDateTime != null + // ? "Delivery Time: ${controller.formatDate(controller.parcelOrder.value.receiverPickupDateTime!)}" + // : '', + isDark, + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // Distance, Weight, Rate + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _iconTile("${controller.parcelOrder.value.distance ?? '--'} ${'KM'.tr}", "Distance".tr, "assets/icons/ic_distance_parcel.svg", isDark), + _iconTile(controller.parcelOrder.value.parcelWeight ?? '--', "Weight".tr, "assets/icons/ic_weight_parcel.svg", isDark), + _iconTile(Constant.amountShow(amount: controller.parcelOrder.value.subTotal), "Rate".tr, "assets/icons/ic_rate_parcel.svg", isDark), + ], + ), + ), + + const SizedBox(height: 10), + + Row( + children: [ + Expanded(child: Text("Coupons".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + InkWell( + onTap: () { + Get.to(ParcelCouponScreen())!.then((value) { + if (value != null) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: value); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = value; + controller.calculatePrice(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } + }); + }, + child: Text( + "View All".tr, + style: AppThemeData.boldTextStyle(decoration: TextDecoration.underline, fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 5), + + // Coupon input + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(10), color: isDark ? AppThemeData.parcelServiceDark300 : AppThemeData.primary300), + child: Container( + decoration: BoxDecoration(color: AppThemeData.parcelService50, borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_coupon_parcel.svg", height: 28, width: 28), + SizedBox(width: 15), + Expanded( + child: TextField( + controller: controller.couponController.value, + style: AppThemeData.semiBoldTextStyle(color: AppThemeData.parcelService500, fontSize: 16), + decoration: InputDecoration( + hintText: "Write coupon code".tr, + hintStyle: AppThemeData.mediumTextStyle(fontSize: 16, color: AppThemeData.parcelService500), + border: InputBorder.none, + ), + ), + ), + RoundedButtonFill( + title: "Redeem now".tr, + onPress: () { + if (controller.couponList.where((element) => element.code!.toLowerCase() == controller.couponController.value.text.toLowerCase()).isNotEmpty) { + CouponModel couponModel = controller.couponList.firstWhere((p0) => p0.code!.toLowerCase() == controller.couponController.value.text.toLowerCase()); + if (couponModel.expiresAt!.toDate().isAfter(DateTime.now())) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: couponModel); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = couponModel; + controller.calculatePrice(); + controller.update(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } else { + ShowToastDialog.showToast("This coupon code has been expired".tr); + } + } else { + ShowToastDialog.showToast("Invalid coupon code".tr); + } + }, + borderRadius: 10, + height: 4, + width: 28, + fontSizes: 14, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey500)), + const SizedBox(height: 8), + + // Subtotal + _summaryTile("Subtotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark, null), + + // Discount + _summaryTile("Discount".tr, "-${Constant.amountShow(amount: controller.discount.value.toString())}", isDark, AppThemeData.dangerDark300), + + // Tax List + ...List.generate(Constant.taxList.length, (index) { + final taxModel = Constant.taxList[index]; + final taxTitle = "${taxModel.title} ${taxModel.type == 'fix' ? '(${Constant.amountShow(amount: taxModel.tax)})' : '(${taxModel.tax}%)'}"; + final taxAmount = Constant.getTaxValue(amount: (controller.subTotal.value - controller.discount.value).toString(), taxModel: taxModel).toString(); + + return _summaryTile(taxTitle, Constant.amountShow(amount: taxAmount), isDark, null); + }), + + const Divider(), + + // Total + _summaryTile("Order Total".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark, null), + ], + ), + ), + const SizedBox(height: 24), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + Text("Payment by".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 12), + + // Row with Sender and Receiver options + Row( + children: [ + // Sender + GestureDetector( + onTap: () => controller.paymentBy.value = "Sender", + child: Row( + children: [ + Icon( + controller.paymentBy.value == "Sender" ? Icons.radio_button_checked : Icons.radio_button_off, + color: controller.paymentBy.value == "Sender" ? AppThemeData.primary300 : (isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + size: 20, + ), + const SizedBox(width: 6), + Text("Sender".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + ], + ), + ), + + const SizedBox(width: 60), + + // Receiver + GestureDetector( + onTap: () => controller.paymentBy.value = "Receiver", + child: Row( + children: [ + Icon( + controller.paymentBy.value == "Receiver" ? Icons.radio_button_checked : Icons.radio_button_off, + color: controller.paymentBy.value == "Receiver" ? AppThemeData.primary300 : (isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + size: 20, + ), + const SizedBox(width: 6), + Text("Receiver".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + ], + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Continue button + RoundedButtonFill( + title: controller.paymentBy.value == "Sender" ? "Select Payment Method".tr : "Continue".tr, + onPress: () async { + if (controller.paymentBy.value == "Sender") { + Get.bottomSheet( + paymentBottomSheet(context, controller, isDark), // your widget + isScrollControlled: true, // ✅ allows full drag scrolling + backgroundColor: Colors.transparent, // so your rounded corners are visible + ); + } else { + controller.placeOrder(); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ); + }, + ); + } + + Widget _infoSection(String title, String name, String address, String phone, bool isDark) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(name, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(address, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(phone, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + // Text(time, style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ); + } + + Widget _iconTile(String value, title, icon, bool isDark) { + return Column( + children: [ + // Icon(icon, color: AppThemeData.primary300), + SvgPicture.asset(icon, height: 28, width: 28, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + const SizedBox(height: 6), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + const SizedBox(height: 6), + Text(title, style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ); + } + + Widget _summaryTile(String title, String value, bool isDark, Color? colors) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: title == "Order Total" ? 18 : 16, color: colors ?? (isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + ], + ), + ); + } + + Widget paymentBottomSheet(BuildContext context, ParcelOrderConfirmationController controller, bool isDark) { + return DraggableScrollableSheet( + initialChildSize: 0.70, + minChildSize: 0.30, + maxChildSize: 0.8, + expand: false, + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey500 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Select Payment Method".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Icon(Icons.close), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: ListView( + controller: scrollController, + children: [ + Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) const SizedBox(height: 10), + Text("Other Payment Options".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.taxiBooking300, + textColor: AppThemeData.grey900, + onPress: () async { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.totalAmount.value.toString(), context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.placeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + double walletBalance = double.tryParse(controller.userModel.value.walletAmount.toString()) ?? 0.0; + double amountToPay = double.tryParse(controller.totalAmount.value.toString()) ?? 0.0; + if (walletBalance < amountToPay) { + ShowToastDialog.showToast("Insufficient wallet balance".tr); + return; + } + controller.placeOrder(); + } + // else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + // controller.placeOrder(); + // } + else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.totalAmount.value.toString()), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.totalAmount.value.toString(), orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + }, + ), + ], + ), + ); + }, + ); + } + + Obx cardDecoration(controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: Constant.userModel?.walletAmount == null ? '0.0' : Constant.userModel?.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen_ui/parcel_service/parcel_order_details.dart b/lib/screen_ui/parcel_service/parcel_order_details.dart new file mode 100644 index 0000000..b3f6fd9 --- /dev/null +++ b/lib/screen_ui/parcel_service/parcel_order_details.dart @@ -0,0 +1,539 @@ +import 'package:customer/screen_ui/parcel_service/parcel_review_screen.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/parcel_order_details_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/user_model.dart'; +import '../../service/fire_store_utils.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_border.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../utils/network_image_widget.dart'; +import '../multi_vendor_service/chat_screens/chat_screen.dart'; + +class ParcelOrderDetails extends StatelessWidget { + const ParcelOrderDetails({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: ParcelOrderDetailsController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Details".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + Text( + "Your parcel is on the way. Track it in real time below.".tr, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + width: double.infinity, + padding: const EdgeInsets.all(16), + child: Text( + "${'Order Id:'.tr} ${Constant.orderId(orderId: controller.parcelOrder.value.id.toString())}".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 18, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Timeline with icons and line + Column( + children: [ + Image.asset("assets/images/image_parcel.png", height: 32, width: 32), + DottedBorder( + options: CustomPathDottedBorderOptions( + color: Colors.grey.shade400, + strokeWidth: 2, + dashPattern: [4, 4], + customPath: + (size) => + Path() + ..moveTo(size.width / 2, 0) + ..lineTo(size.width / 2, size.height), + ), + child: const SizedBox(width: 20, height: 95), + ), + Image.asset("assets/images/image_parcel.png", height: 32, width: 32), + ], + ), + const SizedBox(width: 12), + // Address Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _infoSection( + "Pickup Address (Sender):".tr, + controller.parcelOrder.value.sender?.name ?? '', + controller.parcelOrder.value.sender?.address ?? '', + controller.parcelOrder.value.sender?.phone ?? '', + // controller.parcelOrder.value.senderPickupDateTime != null + // ? "Pickup Time: ${controller.formatDate(controller.parcelOrder.value.senderPickupDateTime!)}" + // : '', + isDark, + ), + const SizedBox(height: 16), + _infoSection( + "Delivery Address (Receiver):".tr, + controller.parcelOrder.value.receiver?.name ?? '', + controller.parcelOrder.value.receiver?.address ?? '', + controller.parcelOrder.value.receiver?.phone ?? '', + // controller.parcelOrder.value.receiverPickupDateTime != null + // ? "Delivery Time: ${controller.formatDate(controller.parcelOrder.value.receiverPickupDateTime!)}" + // : '', + isDark, + ), + ], + ), + ), + ], + ), + + const Divider(), + + if (controller.parcelOrder.value.isSchedule == true) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${'Schedule Pickup time:'.tr} ${controller.formatDate(controller.parcelOrder.value.senderPickupDateTime!)}", + style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.info400), + ), + ), + + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${'Order Date:'.tr}${controller.parcelOrder.value.isSchedule == true ? controller.formatDate(controller.parcelOrder.value.createdAt!) : controller.formatDate(controller.parcelOrder.value.senderPickupDateTime!)}", + style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.info400), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Parcel Type:".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Row( + children: [ + Text( + controller.parcelOrder.value.parcelType ?? '', + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + ), + const SizedBox(width: 8), + if (controller.getSelectedCategory()?.image != null && controller.getSelectedCategory()!.image!.isNotEmpty) + NetworkImageWidget(imageUrl: controller.getSelectedCategory()?.image ?? '', height: 20, width: 20), + ], + ), + ], + ), + controller.parcelOrder.value.parcelImages!.isEmpty + ? SizedBox() + : SizedBox( + height: 120, + child: ListView.builder( + itemCount: controller.parcelOrder.value.parcelImages!.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget(imageUrl: controller.parcelOrder.value.parcelImages![index], width: 100, fit: BoxFit.cover, borderRadius: 10), + ), + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // Distance, Weight, Rate + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _iconTile("${controller.parcelOrder.value.distance ?? '--'} ${Constant.distanceType}", "Distance".tr, "assets/icons/ic_distance_parcel.svg", isDark), + _iconTile(controller.parcelOrder.value.parcelWeight ?? '--', "Weight".tr, "assets/icons/ic_weight_parcel.svg", isDark), + _iconTile(Constant.amountShow(amount: controller.parcelOrder.value.subTotal), "Rate".tr, "assets/icons/ic_rate_parcel.svg", isDark), + ], + ), + ), + const SizedBox(height: 16), + if (controller.parcelOrder.value.driver != null) + Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("About Driver".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + SizedBox( + width: 52, + height: 52, + child: ClipRRect( + borderRadius: BorderRadiusGeometry.circular(10), + child: NetworkImageWidget(imageUrl: controller.driverUser.value?.profilePictureURL ?? '', height: 70, width: 70, borderRadius: 35), + ), + ), + SizedBox(width: 20), + Text( + controller.parcelOrder.value.driver?.fullName() ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 18), + ), + ], + ), + RoundedButtonBorder( + title: controller.driverUser.value!.averageRating.toStringAsFixed(1), + width: 20, + height: 3.5, + radius: 10, + isRight: false, + isCenter: true, + textColor: AppThemeData.warning400, + borderColor: AppThemeData.warning400, + color: AppThemeData.warning50, + icon: SvgPicture.asset("assets/icons/ic_start.svg"), + onPress: () {}, + ), + ], + ), + Visibility( + visible: controller.parcelOrder.value.status == Constant.orderCompleted ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: controller.ratingModel.value.id != null && controller.ratingModel.value.id!.isNotEmpty ? 'Update Review'.tr : 'Add Review'.tr, + onPress: () async { + final result = await Get.to(() => ParcelReviewScreen(), arguments: {'order': controller.parcelOrder.value}); + + // If review was submitted successfully + if (result == true) { + await controller.fetchDriverDetails(); + } + }, + height: 5, + borderRadius: 15, + color: Colors.orange, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + ), + ), + if (controller.parcelOrder.value.status != Constant.orderCompleted) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Constant.makePhoneCall(controller.parcelOrder.value.driver!.phoneNumber.toString()); + }, + child: Container( + width: 150, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.parcelOrder.value.authorID ?? ''); + UserModel? driverUser = await FireStoreUtils.getUserProfile(controller.parcelOrder.value.driverId ?? ''); + + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer?.fullName(), + "restaurantName": driverUser?.fullName(), + "orderId": controller.parcelOrder.value.id, + "restaurantId": driverUser?.id, + "customerId": customer?.id, + "customerProfileImage": customer?.profilePictureURL, + "restaurantProfileImage": driverUser?.profilePictureURL, + "token": driverUser?.fcmToken, + "chatType": "Driver", + }, + ); + }, + child: Container( + width: 150, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 15), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey500)), + const SizedBox(height: 8), + + // Subtotal + _summaryTile("Subtotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark), + + // Discount + _summaryTile("Discount".tr, Constant.amountShow(amount: controller.discount.value.toString()), isDark), + + // Tax List + ...List.generate(controller.parcelOrder.value.taxSetting!.length, (index) { + return _summaryTile( + "${controller.parcelOrder.value.taxSetting![index].title} ${controller.parcelOrder.value.taxSetting![index].type == 'fix' ? '' : '(${controller.parcelOrder.value.taxSetting![index].tax}%)'}", + Constant.amountShow( + amount: + Constant.getTaxValue( + amount: + ((double.tryParse(controller.parcelOrder.value.subTotal.toString()) ?? 0.0) - (double.tryParse(controller.parcelOrder.value.discount.toString()) ?? 0.0)) + .toString(), + taxModel: controller.parcelOrder.value.taxSetting![index], + ).toString(), + ), + isDark, + ); + }), + + const Divider(), + + // Total + _summaryTile("Order Total".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark), + ], + ), + ), + ], + ), + ), + bottomNavigationBar: + controller.parcelOrder.value.status == Constant.orderPlaced + ? Padding( + padding: const EdgeInsets.all(16.0), + child: RoundedButtonFill( + title: "Cancel Parcel".tr, + onPress: () { + controller.cancelParcelOrder(); + }, + height: 5, + borderRadius: 15, + color: AppThemeData.primary300, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + ) + : SizedBox(), + ); + }, + ); + } + + Widget statusBottomSheet(BuildContext context, ParcelOrderDetailsController controller, bool isDark) { + return DraggableScrollableSheet( + initialChildSize: 0.30, + minChildSize: 0.20, + maxChildSize: 0.6, + expand: false, + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey500 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: SingleChildScrollView( + controller: scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Parcel Status Timeline".tr, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 18)), + const SizedBox(height: 8), + + // Dynamic List + Obx(() { + final history = controller.parcelOrder.value.statusHistory ?? []; + + if (history.isEmpty) { + return SizedBox( + height: 80, + child: Center(child: Text("No status updates yet".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + ); + } + + return SizedBox( + height: history.length * 70.0, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: history.length, + itemBuilder: (context, index) { + final statusUpdate = history[index]; + final isCompleted = index < history.length; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset(isCompleted ? "assets/images/image_status_timeline.png" : "assets/images/image_timeline.png", height: 48, width: 48), + const SizedBox(width: 20), + Expanded( + child: Text( + statusUpdate.status ?? '', + style: AppThemeData.semiBoldTextStyle(color: isCompleted ? (isDark ? AppThemeData.greyDark900 : AppThemeData.grey900) : AppThemeData.grey500, fontSize: 18), + ), + ), + ], + ), + ); + }, + ), + ); + }), + ], + ), + ), + ); + }, + ); + } + + Widget _infoSection(String title, String name, String address, String phone, bool isDark) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(name, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(address, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Text(phone, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + // Text(time, style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ); + } + + Widget _iconTile(String value, title, icon, bool isDark) { + return Column( + children: [ + // Icon(icon, color: AppThemeData.primary300), + SvgPicture.asset(icon, height: 28, width: 28, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800), + const SizedBox(height: 6), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + const SizedBox(height: 6), + Text(title, style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ); + } + + Widget _summaryTile(String title, String value, bool isDark) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: title == "Order Total".tr ? 18 : 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ], + ), + ); + } +} diff --git a/lib/screen_ui/parcel_service/parcel_review_screen.dart b/lib/screen_ui/parcel_service/parcel_review_screen.dart new file mode 100644 index 0000000..d6620d2 --- /dev/null +++ b/lib/screen_ui/parcel_service/parcel_review_screen.dart @@ -0,0 +1,145 @@ +import 'package:customer/controllers/parcel_review_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import '../../utils/network_image_widget.dart'; + +class ParcelReviewScreen extends StatelessWidget { + const ParcelReviewScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: ParcelReviewController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + elevation: 0, + backgroundColor: AppThemeData.primary300, + leading: GestureDetector(onTap: () => Get.back(), child: Icon(Icons.arrow_back_ios, color: isDark ? Colors.white : Colors.black)), + title: Text( + controller.ratingModel.value != null && controller.ratingModel.value!.id!.isNotEmpty ? "Update Review".tr : "Add Review".tr, + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 16), + ), + ), + body: Obx( + () => + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.only(top: 20), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 50, bottom: 20), + child: Card( + elevation: 2, + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 65), + child: Column( + children: [ + // Driver Name + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + controller.order.value!.driver?.fullName() ?? "", + style: TextStyle(color: isDark ? Colors.white : Colors.black87, fontFamily: AppThemeData.medium, fontSize: 18), + ), + ), + // Car info + const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: Divider(color: Colors.grey)), + + // Title + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text('How is your trip?'.tr, style: TextStyle(fontSize: 18, color: isDark ? Colors.white : Colors.black, fontWeight: FontWeight.bold, letterSpacing: 2)), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'Your feedback will help us improve \n driving experience better'.tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? Colors.white : Colors.black.withOpacity(0.60), letterSpacing: 0.8), + ), + ), + + // Rating + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text('Rate for'.tr, style: TextStyle(fontSize: 16, color: isDark ? Colors.white : Colors.black.withOpacity(0.60), letterSpacing: 0.8)), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + controller.order.value!.driver?.fullName() ?? "", + style: TextStyle(fontSize: 18, color: isDark ? Colors.white : Colors.black, fontWeight: FontWeight.bold, letterSpacing: 2), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: RatingBar.builder( + initialRating: controller.ratings.value, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemBuilder: (context, _) => const Icon(Icons.star, color: Colors.amber), + unratedColor: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400, + onRatingUpdate: (rating) => controller.ratings.value = rating, + ), + ), + + // Comment + Padding(padding: const EdgeInsets.all(20.0), child: TextFieldWidget(hintText: "Type comment....".tr, controller: controller.comment.value, maxLine: 5)), + + // Submit Button + Padding( + padding: const EdgeInsets.all(20.0), + child: RoundedButtonFill( + title: controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, + color: AppThemeData.primary300, + textColor: isDark ? Colors.white : Colors.black, + onPress: controller.submitReview, + ), + ), + ], + ), + ), + ), + ), + ), + Align( + alignment: Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.15), blurRadius: 8, spreadRadius: 6)], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(60), + child: NetworkImageWidget(imageUrl: controller.order.value?.driver?.profilePictureURL ?? '', fit: BoxFit.cover, height: 110, width: 110), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/rental_service/my_rental_booking_screen.dart b/lib/screen_ui/rental_service/my_rental_booking_screen.dart new file mode 100644 index 0000000..268124b --- /dev/null +++ b/lib/screen_ui/rental_service/my_rental_booking_screen.dart @@ -0,0 +1,332 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/models/rental_order_model.dart'; +import 'package:customer/screen_ui/auth_screens/login_screen.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/screen_ui/rental_service/rental_order_details_screen.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/my_rental_booking_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; + +class MyRentalBookingScreen extends StatelessWidget { + const MyRentalBookingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: MyRentalBookingController(), + builder: (controller) { + return DefaultTabController( + length: controller.tabTitles.length, + initialIndex: controller.tabTitles.indexOf(controller.selectedTab.value), + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row(children: [const SizedBox(width: 10), Text("Rental History".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900))]), + ), + bottom: TabBar( + onTap: (index) { + controller.selectTab(controller.tabTitles[index]); + }, + indicatorColor: AppThemeData.parcelService500, + labelColor: AppThemeData.parcelService500, + unselectedLabelColor: AppThemeData.parcelService500, + labelStyle: AppThemeData.boldTextStyle(fontSize: 13), + unselectedLabelStyle: AppThemeData.mediumTextStyle(fontSize: 13), + tabs: controller.tabTitles.map((title) => Tab(child: Center(child: Text(title)))).toList(), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : Constant.userModel == null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)), + const SizedBox(height: 5), + Text( + "You’re not logged in. Please sign in to access your account and explore all features.".tr, + textAlign: TextAlign.center, + style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold), + ), + const SizedBox(height: 20), + RoundedButtonFill( + title: "Log in".tr, + width: 55, + height: 5.5, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + onPress: () async { + Get.offAll(const LoginScreen()); + }, + ), + ], + ), + ) + : TabBarView( + children: + controller.tabTitles.map((title) { + List orders = controller.getOrdersForTab(title); + + if (orders.isEmpty) { + return Center(child: Text("No orders found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))); + } + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: orders.length, + itemBuilder: (context, index) { + RentalOrderModel order = orders[index]; //use this + return InkWell( + onTap: () { + Get.to(() => RentalOrderDetailsScreen(), arguments: order); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding(padding: const EdgeInsets.only(top: 5), child: Image.asset("assets/icons/pickup.png", height: 18, width: 18)), + const SizedBox(width: 10), + Expanded( + //prevents overflow + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + //text wraps if too long + child: Text( + order.sourceLocationName ?? "-", + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + overflow: TextOverflow.ellipsis, //safe cutoff + maxLines: 2, + ), + ), + if (order.status != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + decoration: BoxDecoration( + color: AppThemeData.info50, + border: Border.all(color: AppThemeData.info300), + borderRadius: BorderRadius.circular(12), + ), + child: Text(order.status ?? '', style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.info500)), + ), + ], + ], + ), + if (order.bookingDateTime != null) + Text( + Constant.timestampToDateTime(order.bookingDateTime!), + style: AppThemeData.mediumTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Text("Vehicle Type :".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Row( + children: [ + ClipRRect( + //borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: order.rentalVehicleType!.rentalVehicleIcon.toString(), + height: 60, + width: 60, + imageBuilder: + (context, imageProvider) => Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover)), + ), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover), + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${order.rentalVehicleType!.name}", + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + "${order.rentalVehicleType!.shortDescription}", + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ), + ], + ), + ), + ), + ], + ), + ), + Text("Package info :".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.rentalPackageModel!.name.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 4), + Text( + order.rentalPackageModel!.description.toString(), + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + SizedBox(width: 10), + Text( + Constant.amountShow(amount: order.rentalPackageModel!.baseFare.toString()), + style: AppThemeData.boldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + if (Constant.isEnableOTPTripStartForRental == true) + Text("${'OTP :'.tr} ${order.otpCode}", style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + SizedBox(height: 10), + Row( + children: [ + order.status == Constant.orderInTransit && order.paymentStatus == false + ? Expanded( + child: RoundedButtonFill( + title: "Pay Now", + onPress: () { + Get.to(() => RentalOrderDetailsScreen(), arguments: order); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ) + : SizedBox(), + order.status == Constant.orderPlaced || order.status == Constant.driverAccepted + ? Expanded( + child: RoundedButtonFill( + title: "Cancel Booking", + onPress: () => controller.cancelRentalRequest(order, taxList: order.taxSetting), + color: AppThemeData.danger300, + textColor: AppThemeData.surface, + ), + ) + : SizedBox(), + ], + ), + ], + ), + ), + ); + }, + ); + }).toList(), + ), + ), + ); + }, + ); + } + + Obx cardDecoration(MyRentalBookingController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: Constant.userModel!.walletAmount == null ? '0.0' : Constant.userModel!.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/rental_service/rental_conformation_screen.dart b/lib/screen_ui/rental_service/rental_conformation_screen.dart new file mode 100644 index 0000000..34e9b3e --- /dev/null +++ b/lib/screen_ui/rental_service/rental_conformation_screen.dart @@ -0,0 +1,332 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/screen_ui/rental_service/rental_coupon_screen.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import '../../controllers/rental_conformation_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; + +class RentalConformationScreen extends StatelessWidget { + const RentalConformationScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RentalConformationController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Confirm Rent a Car".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Center(child: Constant.loader()) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset("assets/icons/pickup.png", height: 15, width: 15), + SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${controller.rentalOrderModel.value.sourceLocationName}", + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Text( + Constant.timestampToDate(controller.rentalOrderModel.value.bookingDateTime!), + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Your Preference".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.rentalOrderModel.value.rentalPackageModel!.name.toString(), + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 4), + Text( + controller.rentalOrderModel.value.rentalPackageModel!.description.toString(), + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + SizedBox(width: 10), + Text( + Constant.amountShow(amount: controller.rentalOrderModel.value.rentalPackageModel!.baseFare.toString()), + style: AppThemeData.boldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ], + ), + ), + SizedBox(height: 20), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Vehicle Type".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadiusGeometry.circular(10), + child: NetworkImageWidget(imageUrl: controller.rentalOrderModel.value.rentalVehicleType!.rentalVehicleIcon.toString(), height: 50, width: 50, borderRadius: 10), + ), + SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${controller.rentalOrderModel.value.rentalVehicleType!.name}", + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Text( + "${controller.rentalOrderModel.value.rentalVehicleType!.shortDescription}", + style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + ], + ), + ], + ), + ), + SizedBox(height: 10), + + Row( + children: [ + Expanded(child: Text("Coupons".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + InkWell( + onTap: () { + Get.to(RentalCouponScreen())!.then((value) { + if (value != null) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: value); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = value; + controller.calculateAmount(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } + }); + }, + child: Text( + "View All".tr, + style: AppThemeData.boldTextStyle(decoration: TextDecoration.underline, fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 5), + + // Coupon input + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(10), color: isDark ? AppThemeData.parcelServiceDark300 : AppThemeData.primary300), + child: Container( + decoration: BoxDecoration(color: AppThemeData.parcelService50, borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + SvgPicture.asset("assets/icons/ic_coupon_parcel.svg", height: 28, width: 28), + SizedBox(width: 15), + Expanded( + child: TextFormField( + controller: controller.couponController.value, + style: AppThemeData.semiBoldTextStyle(color: AppThemeData.grey900), + decoration: InputDecoration( + hintText: "Write coupon code".tr, + hintStyle: AppThemeData.mediumTextStyle(fontSize: 16, color: AppThemeData.parcelService500), + border: InputBorder.none, + ), + ), + ), + RoundedButtonFill( + title: "Redeem now".tr, + onPress: () { + if (controller.couponList.where((element) => element.code!.toLowerCase() == controller.couponController.value.text.toLowerCase()).isNotEmpty) { + CouponModel couponModel = controller.couponList.firstWhere((p0) => p0.code!.toLowerCase() == controller.couponController.value.text.toLowerCase()); + if (couponModel.expiresAt!.toDate().isAfter(DateTime.now())) { + double couponAmount = Constant.calculateDiscount(amount: controller.subTotal.value.toString(), offerModel: couponModel); + if (couponAmount < controller.subTotal.value) { + controller.selectedCouponModel.value = couponModel; + controller.calculateAmount(); + controller.update(); + } else { + ShowToastDialog.showToast("This offer not eligible for this booking".tr); + } + } else { + ShowToastDialog.showToast("This coupon code has been expired".tr); + } + } else { + ShowToastDialog.showToast("Invalid coupon code".tr); + } + }, + borderRadius: 10, + height: 4, + width: 28, + fontSizes: 14, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey500)), + const SizedBox(height: 8), + + // Subtotal + _summaryTile("Subtotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark, null), + + // Discount + _summaryTile("Discount".tr, Constant.amountShow(amount: controller.discount.value.toString()), isDark, AppThemeData.dangerDark300), + + // Tax List + ...List.generate(controller.rentalOrderModel.value.taxSetting!.length, (index) { + final taxModel = controller.rentalOrderModel.value.taxSetting![index]; + final taxTitle = "${taxModel.title} ${taxModel.type == 'fix' ? '(${Constant.amountShow(amount: taxModel.tax)})' : '(${taxModel.tax}%)'}"; + + return _summaryTile( + taxTitle, + Constant.amountShow( + amount: + Constant.getTaxValue( + amount: (controller.subTotal.value - controller.discount.value).toString(), + taxModel: controller.rentalOrderModel.value.taxSetting![index], + ).toString(), + ), + isDark, + null, + ); + }), + + const Divider(), + + // Total + _summaryTile("Order Total".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark, null), + ], + ), + ), + SizedBox(height: 20), + RoundedButtonFill( + title: "Book now".tr, + onPress: () { + controller.placeOrder(); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + SizedBox(height: 20), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _summaryTile(String title, String value, bool isDark, Color? colors) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: title == "Order Total" ? 18 : 16, color: colors ?? (isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + ], + ), + ); + } +} diff --git a/lib/screen_ui/rental_service/rental_coupon_screen.dart b/lib/screen_ui/rental_service/rental_coupon_screen.dart new file mode 100644 index 0000000..b721549 --- /dev/null +++ b/lib/screen_ui/rental_service/rental_coupon_screen.dart @@ -0,0 +1,138 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/rental_coupon_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/models/coupon_model.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/widget/my_separator.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class RentalCouponScreen extends StatelessWidget { + const RentalCouponScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RentalCouponController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Coupon".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Constant.loader() + : controller.cabCouponList.isEmpty + ? Constant.showEmptyView(message: "Coupon not found".tr) + : ListView.builder( + shrinkWrap: true, + itemCount: controller.cabCouponList.length, + itemBuilder: (context, index) { + CouponModel couponModel = controller.cabCouponList[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Container( + height: Responsive.height(16, context), + decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)), + child: Stack( + children: [ + Image.asset("assets/images/ic_coupon_image.png", height: Responsive.height(16, context), fit: BoxFit.fill), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Align( + alignment: Alignment.center, + child: RotatedBox( + quarterTurns: -1, + child: Text( + "${couponModel.discountType == "Fix Price" ? Constant.amountShow(amount: couponModel.discount) : "${couponModel.discount}%"} ${'Off'.tr}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey50), + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + DottedBorder( + options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(6), color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "${couponModel.code}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500), + ), + ), + ), + const Expanded(child: SizedBox(height: 10)), + InkWell( + onTap: () { + Get.back(result: couponModel); + }, + child: Text( + "Tap To Apply".tr, + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ), + ], + ), + const SizedBox(height: 20), + MySeparator(color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + const SizedBox(height: 20), + Text( + "${couponModel.description}", + textAlign: TextAlign.start, + style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/rental_service/rental_dashboard_screen.dart b/lib/screen_ui/rental_service/rental_dashboard_screen.dart new file mode 100644 index 0000000..99a24c9 --- /dev/null +++ b/lib/screen_ui/rental_service/rental_dashboard_screen.dart @@ -0,0 +1,81 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/cab_dashboard_controller.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import '../../controllers/cab_rental_dashboard_controllers.dart'; + +class RentalDashboardScreen extends StatelessWidget { + const RentalDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return Obx(() { + final isDark = themeController.isDark.value; + return GetX( + init: CabRentalDashboardControllers(), + builder: (controller) { + return Scaffold( + body: controller.pageList[controller.selectedIndex.value], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + showUnselectedLabels: true, + showSelectedLabels: true, + selectedFontSize: 12, + selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold), + currentIndex: controller.selectedIndex.value, + backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50, + selectedItemColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + unselectedItemColor: isDark ? AppThemeData.grey300 : AppThemeData.grey600, + onTap: (int index) { + if (index == 0) { + Get.put(CabDashboardController()); + } + controller.selectedIndex.value = index; + }, + items: + Constant.walletSetting == false + ? [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ] + : [ + navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller), + navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller), + navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_wallet_cab.svg", label: 'Wallet'.tr, controller: controller), + navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller), + ], + ), + ); + }, + ); + }); + } + + BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required CabRentalDashboardControllers controller}) { + return BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: SvgPicture.asset( + assetIcon, + height: 22, + width: 22, + color: + controller.selectedIndex.value == index + ? isDark + ? AppThemeData.primary300 + : AppThemeData.primary300 + : isDark + ? AppThemeData.grey300 + : AppThemeData.grey600, + ), + ), + label: label, + ); + } +} diff --git a/lib/screen_ui/rental_service/rental_home_screen.dart b/lib/screen_ui/rental_service/rental_home_screen.dart new file mode 100644 index 0000000..94e8b62 --- /dev/null +++ b/lib/screen_ui/rental_service/rental_home_screen.dart @@ -0,0 +1,618 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/rental_home_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/rental_vehicle_type.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/show_toast_dialog.dart'; +import '../../themes/text_field_widget.dart'; +import '../../utils/utils.dart'; +import '../../widget/osm_map/map_picker_page.dart'; +import '../../widget/place_picker/location_picker_screen.dart'; +import '../../widget/place_picker/selected_location_model.dart'; +import '../auth_screens/login_screen.dart'; +import '../multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as latlong; + +class RentalHomeScreen extends StatelessWidget { + const RentalHomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: RentalHomeController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Constant.userModel == null + ? InkWell( + onTap: () { + Get.offAll(const LoginScreen()); + }, + child: Text("Login".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900)), + ) + : Text(Constant.userModel!.fullName(), style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900)), + ], + ), + ), + ], + ), + ), + ), + body: Stack( + children: [ + controller.isLoading.value + ? Center(child: Constant.loader()) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + InkWell( + onTap: () async { + if (Constant.selectedMapType == 'osm') { + final result = await Get.to(() => MapPickerPage()); + if (result != null) { + final firstPlace = result; + + if (Constant.checkZoneCheck(firstPlace.coordinates.latitude, firstPlace.coordinates.longitude) == true) { + final address = firstPlace.address; + final lat = firstPlace.coordinates.latitude; + final lng = firstPlace.coordinates.longitude; + controller.sourceTextEditController.value.text = address; + controller.departureLatLongOsm.value = latlong.LatLng(lat, lng); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + } else { + Get.to(LocationPickerScreen())!.then((value) async { + if (value != null) { + SelectedLocationModel selectedLocationModel = value; + + if (Constant.checkZoneCheck(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude) == true) { + controller.sourceTextEditController.value.text = Utils.formatAddress(selectedLocation: selectedLocationModel); + controller.departureLatLong.value = latlong.LatLng(selectedLocationModel.latLng!.latitude, selectedLocationModel.latLng!.longitude); + } else { + ShowToastDialog.showToast("Service is unavailable at the selected address.".tr); + } + } + }); + } + }, + hoverColor: Colors.transparent, + child: TextFieldWidget( + controller: controller.sourceTextEditController.value, + hintText: "Your current location".tr, + title: "Pickup Location".tr, + enable: false, + prefix: Padding(padding: EdgeInsets.only(left: 10, right: 10), child: Icon(Icons.stop_circle_outlined, color: Colors.green)), + ), + ), + const SizedBox(height: 25), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Select Your Vehicle Type".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(width: 10), + GestureDetector( + onTap: () => controller.pickDate(context), + child: Row( + children: [ + Text( + "${controller.selectedDate.value.day}-${controller.selectedDate.value.month}-${controller.selectedDate.value.year}", + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(width: 5), + const Icon(Icons.date_range, size: 20), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + ListView.builder( + itemCount: controller.vehicleTypes.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + RentalVehicleType vehicleType = controller.vehicleTypes[index]; + return Obx( + () => InkWell( + onTap: () { + controller.selectedVehicleType.value = controller.vehicleTypes[index]; + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: + isDark + ? controller.selectedVehicleType.value?.id == vehicleType.id + ? AppThemeData.carRentDark300 + : Colors.transparent + : controller.selectedVehicleType.value?.id == vehicleType.id + ? AppThemeData.carRent300 + : Colors.transparent, + width: 1, + ), + color: + controller.selectedVehicleType.value?.id == vehicleType.id + ? AppThemeData.carRent50 + : isDark + ? AppThemeData.carRentDark50 + : Colors.white, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Row( + children: [ + ClipRRect( + //borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: vehicleType.rentalVehicleIcon.toString(), + height: 60, + width: 60, + imageBuilder: + (context, imageProvider) => + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))), + placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))), + errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover), + fit: BoxFit.cover, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${vehicleType.name}", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1, + color: + isDark + ? controller.selectedVehicleType.value?.id == vehicleType.id + ? AppThemeData.greyDark50 + : AppThemeData.grey50 + : AppThemeData.greyDark50, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + "${vehicleType.description}", + style: TextStyle( + fontWeight: FontWeight.w400, + letterSpacing: 1, + color: + isDark + ? controller.selectedVehicleType.value?.id == vehicleType.id + ? AppThemeData.greyDark50 + : AppThemeData.grey50 + : AppThemeData.greyDark50, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 25), + RoundedButtonFill( + title: "Continue".tr, + onPress: () async { + final sourceText = controller.sourceTextEditController.value.text.trim(); + if (Constant.userModel == null) { + ShowToastDialog.showToast("Please login to continue".tr); + return; + } + if (sourceText.isEmpty) { + ShowToastDialog.showToast("Please select source location".tr); + return; + } + + if (controller.selectedVehicleType.value == null) { + ShowToastDialog.showToast("Please select a vehicle type".tr); + return; + } + + await controller.getRentalPackage(); + + if (controller.rentalPackages.isEmpty) { + ShowToastDialog.showToast("No preference available for the selected vehicle type".tr); + return; + } + + // Open bottom sheet if packages exist + Get.bottomSheet(selectPreferences(context, controller, isDark), isScrollControlled: true, backgroundColor: Colors.transparent); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget selectPreferences(BuildContext context, RentalHomeController controller, bool isDark) { + return DraggableScrollableSheet( + initialChildSize: 0.40, + minChildSize: 0.40, + maxChildSize: 0.7, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration(color: isDark ? Colors.black : Colors.white, borderRadius: const BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: Column( + children: [ + // handle bar + Container(height: 4, width: 33, decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: Colors.grey.shade400)), + + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text("Select Preferences".tr, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: isDark ? Colors.white : Colors.black)), + ), + ), + + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: controller.rentalPackages.length, + itemBuilder: (context, index) { + final package = controller.rentalPackages[index]; + return Obx( + () => InkWell( + onTap: () => controller.selectedPackage.value = package, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: controller.selectedPackage.value?.id == package.id ? (isDark ? AppThemeData.carRentDark300 : AppThemeData.carRent300) : Colors.transparent, + width: 1, + ), + color: + controller.selectedPackage.value?.id == package.id + ? AppThemeData.carRent50 + : isDark + ? AppThemeData.carRentDark50 + : Colors.white, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + package.name ?? "", + style: AppThemeData.semiBoldTextStyle( + fontSize: 18, + color: + isDark + ? controller.selectedPackage.value?.id == package.id + ? AppThemeData.greyDark50 + : AppThemeData.grey50 + : AppThemeData.greyDark50, + ), + ), + const SizedBox(height: 4), + Text( + package.description ?? "", + style: AppThemeData.mediumTextStyle( + fontSize: 14, + color: + isDark + ? controller.selectedPackage.value?.id == package.id + ? AppThemeData.greyDark50 + : AppThemeData.grey50 + : AppThemeData.greyDark50, + ), + ), + ], + ), + ), + Text( + Constant.amountShow(amount: package.baseFare.toString()), + style: AppThemeData.boldTextStyle( + fontSize: 18, + color: + isDark + ? controller.selectedPackage.value?.id == package.id + ? AppThemeData.greyDark50 + : AppThemeData.grey50 + : AppThemeData.greyDark50, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + + const SizedBox(height: 10), + + RoundedButtonFill( + title: "Continue".tr, + onPress: () { + Get.bottomSheet(paymentBottomSheet(context, controller, isDark), isScrollControlled: true, backgroundColor: Colors.transparent); + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ], + ), + ), + ); + }, + ); + } + + Widget paymentBottomSheet(BuildContext context, RentalHomeController controller, bool isDark) { + return DraggableScrollableSheet( + initialChildSize: 0.70, + // Start height + minChildSize: 0.30, + // Minimum height + maxChildSize: 0.8, + // Maximum height + expand: false, + // Prevents full-screen takeover + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey500 : Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Select Payment Method".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + GestureDetector(onTap: () => Get.back(), child: const Icon(Icons.close)), + ], + ), + const SizedBox(height: 20), + + // Payment options list + Expanded( + child: ListView( + padding: EdgeInsets.zero, + controller: scrollController, + children: [ + Text("Preferred Payment".tr, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text("Other Payment Options".tr, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + ], + ), + + // Other gateways + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + + // Continue button + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + onPress: () async { + if (controller.selectedPaymentMethod.value.isEmpty) { + ShowToastDialog.showToast("Please select a payment method".tr); + return; + } + + // Only check wallet if payment method is wallet + if (controller.selectedPaymentMethod.value == "wallet") { + num walletAmount = controller.userModel.value.walletAmount ?? 0; + num baseFare = double.tryParse(controller.selectedPackage.value?.baseFare.toString() ?? "0") ?? 0; + + if (walletAmount < baseFare) { + ShowToastDialog.showToast("You do not have sufficient wallet balance".tr); + return; + } + } + // Complete the order + controller.completeOrder(); + }, + ), + ], + ), + ); + }, + ); + } + + Obx cardDecoration(RentalHomeController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: controller.userModel.value.walletAmount == null ? '0.0' : controller.userModel.value.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/rental_service/rental_order_details_screen.dart b/lib/screen_ui/rental_service/rental_order_details_screen.dart new file mode 100644 index 0000000..6457250 --- /dev/null +++ b/lib/screen_ui/rental_service/rental_order_details_screen.dart @@ -0,0 +1,839 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/rental_order_model.dart'; +import 'package:customer/payment/createRazorPayOrderModel.dart'; +import 'package:customer/payment/rozorpayConroller.dart'; +import 'package:customer/screen_ui/multi_vendor_service/wallet_screen/wallet_screen.dart'; +import 'package:customer/screen_ui/rental_service/rental_review_screen.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/show_toast_dialog.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import '../../controllers/rental_order_details_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../models/user_model.dart'; +import '../../service/fire_store_utils.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_border.dart'; +import '../../themes/round_button_fill.dart'; +import '../multi_vendor_service/chat_screens/chat_screen.dart'; + +class RentalOrderDetailsScreen extends StatelessWidget { + const RentalOrderDetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: RentalOrderDetailsController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: AppThemeData.primary300, + title: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50), + child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))), + ), + ), + const SizedBox(width: 10), + Text("Order Details".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)), + ], + ), + ), + ), + body: + controller.isLoading.value + ? Center(child: Constant.loader()) + : Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + "${'Booking Id :'.tr} ${controller.order.value.id}", + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700), + ), + ), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: controller.order.value.id.toString())); + ShowToastDialog.showToast("Booking ID copied to clipboard".tr); + }, + child: Icon(Icons.copy), + ), + ], + ), + SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding(padding: const EdgeInsets.only(top: 5), child: Image.asset("assets/icons/pickup.png", height: 15, width: 15)), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.order.value.sourceLocationName ?? "-", + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + if (controller.order.value.bookingDateTime != null) + Text( + Constant.timestampToDate(controller.order.value.bookingDateTime!), + style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 15), + if (controller.order.value.rentalPackageModel != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Your Preference".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + SizedBox(height: 10), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.order.value.rentalPackageModel!.name ?? "-", + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + const SizedBox(height: 4), + Text( + controller.order.value.rentalPackageModel!.description ?? "", + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + const SizedBox(width: 10), + Text( + Constant.amountShow(amount: controller.order.value.rentalPackageModel!.baseFare.toString()), + style: AppThemeData.boldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 15), + if (controller.order.value.driver != null) + Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("About Driver".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: 52, + height: 52, + child: ClipRRect( + borderRadius: BorderRadiusGeometry.circular(10), + child: NetworkImageWidget(imageUrl: controller.driverUser.value?.profilePictureURL ?? '', height: 70, width: 70, borderRadius: 35), + ), + ), + SizedBox(width: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.order.value.driver?.fullName() ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 18), + ), + Text( + "${controller.order.value.driver?.vehicleType ?? ''} | ${controller.order.value.driver?.carMakes.toString()}", + style: TextStyle(fontFamily: AppThemeData.medium, color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 14), + ), + Text( + controller.order.value.driver?.carNumber ?? '', + style: AppThemeData.boldTextStyle(color: isDark ? AppThemeData.greyDark700 : AppThemeData.grey700, fontSize: 16), + ), + ], + ), + ], + ), + RoundedButtonBorder( + title: controller.driverUser.value?.averageRating.toString() ?? '', + width: 20, + height: 3.5, + radius: 10, + isRight: false, + isCenter: true, + textColor: AppThemeData.warning400, + borderColor: AppThemeData.warning400, + color: AppThemeData.warning50, + icon: SvgPicture.asset("assets/icons/ic_start.svg"), + onPress: () {}, + ), + ], + ), + Visibility( + visible: controller.order.value.status == Constant.orderCompleted ? true : false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: RoundedButtonFill( + title: controller.ratingModel.value.id != null && controller.ratingModel.value.id!.isNotEmpty ? 'Update Review'.tr : 'Add Review'.tr, + onPress: () async { + final result = await Get.to(() => RentalReviewScreen(), arguments: {'order': controller.order.value}); + + // If review was submitted successfully + if (result == true) { + await controller.fetchDriverDetails(); + } + }, + height: 5, + borderRadius: 15, + color: Colors.orange, + textColor: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, + ), + ), + ), + controller.order.value.status == Constant.orderCompleted || controller.order.value.status == Constant.orderCancelled + ? SizedBox() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Constant.makePhoneCall(controller.order.value.driver!.phoneNumber ?? ''); + }, + child: Container( + width: 150, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_phone_call.svg")), + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + ShowToastDialog.showLoader("Please wait...".tr); + + UserModel? customer = await FireStoreUtils.getUserProfile(controller.order.value.authorID ?? ''); + UserModel? driverUser = await FireStoreUtils.getUserProfile(controller.order.value.driverId ?? ''); + + ShowToastDialog.closeLoader(); + + Get.to( + const ChatScreen(), + arguments: { + "customerName": customer?.fullName(), + "restaurantName": driverUser?.fullName(), + "orderId": controller.order.value.id, + "restaurantId": driverUser?.id, + "customerId": customer?.id, + "customerProfileImage": customer?.profilePictureURL, + "restaurantProfileImage": driverUser?.profilePictureURL, + "token": driverUser?.fcmToken, + "chatType": "Driver", + }, + ); + }, + child: Container( + width: 150, + height: 42, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: isDark ? AppThemeData.grey700 : AppThemeData.grey200), + borderRadius: BorderRadius.circular(120), + ), + ), + child: Padding(padding: const EdgeInsets.all(8.0), child: SvgPicture.asset("assets/icons/ic_wechat.svg")), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 15), + ], + ), + if (controller.order.value.rentalVehicleType != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Vehicle Type".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + SizedBox(height: 10), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: NetworkImageWidget(imageUrl: controller.order.value.rentalVehicleType!.rentalVehicleIcon ?? "", height: 50, width: 50), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.order.value.rentalVehicleType!.name ?? "", + style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + Text( + controller.order.value.rentalVehicleType!.shortDescription ?? "", + style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark600 : AppThemeData.grey600), + ), + ], + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ), + ), + const SizedBox(height: 15), + + Container( + width: Responsive.width(100, context), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Rental Details".tr, style: AppThemeData.boldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + Divider(color: isDark ? AppThemeData.greyDark300 : AppThemeData.grey300), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + 'Rental Package'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + controller.order.value.rentalPackageModel!.name.toString().tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + 'Rental Package Price'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + Constant.amountShow(amount: controller.order.value.rentalPackageModel!.baseFare.toString()).tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + '${'Including'.tr} ${Constant.distanceType.tr}', + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + "${controller.order.value.rentalPackageModel!.includedDistance.toString()} ${Constant.distanceType}".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + 'Including Hours'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + "${controller.order.value.rentalPackageModel!.includedHours.toString()} ${'Hr'.tr}".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + '${'Extra'.tr} ${Constant.distanceType}', + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + controller.getExtraKm(), + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + + // Padding( + // padding: const EdgeInsets.symmetric(vertical: 10), + // child: Row( + // children: [ + // Expanded( + // child: Text( + // 'Extra ${Constant.distanceType}', + // textAlign: TextAlign.start, + // style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + // ), + // ), + // Text( + // "${(double.parse(controller.order.value.endKitoMetersReading!.toString()) - double.parse(controller.order.value.startKitoMetersReading!.toString()) - double.parse(controller.order.value.rentalPackageModel!.includedDistance!.toString()))} ${Constant.distanceType}", + // textAlign: TextAlign.start, + // style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + // ), + // ], + // ), + // ), + controller.order.value.endTime == null + ? SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + 'Extra Minutes'.tr, + textAlign: TextAlign.start, + style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ), + Text( + "${controller.order.value.endTime == null ? "0" : (((controller.order.value.endTime!.toDate().difference(controller.order.value.startTime!.toDate()).inMinutes) - (int.parse(controller.order.value.rentalPackageModel!.includedHours.toString()) * 60)).clamp(0, double.infinity).toInt().toString())} ${'Min'.tr}", + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Order Summary".tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey500)), + const SizedBox(height: 8), + + _summaryTile("Subtotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark, null), + _summaryTile("Discount".tr, Constant.amountShow(amount: controller.discount.value.toString()), isDark, AppThemeData.dangerDark300), + + ...List.generate(controller.order.value.taxSetting?.length ?? 0, (index) { + final taxModel = controller.order.value.taxSetting![index]; + final taxTitle = "${taxModel.title} ${taxModel.type == 'fix' ? '(${Constant.amountShow(amount: taxModel.tax)})' : '(${taxModel.tax}%)'}"; + return _summaryTile( + taxTitle, + Constant.amountShow(amount: Constant.getTaxValue(amount: (controller.subTotal.value - controller.discount.value).toString(), taxModel: taxModel).toString()), + isDark, + null, + ); + }), + + const Divider(), + _summaryTile("Order Total".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark, null), + ], + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + if (controller.order.value.status == Constant.orderInTransit && controller.order.value.paymentStatus == false) + Expanded( + child: RoundedButtonFill( + title: "Pay Now", + onPress: () { + if (controller.order.value.endKitoMetersReading == null || + controller.order.value.endKitoMetersReading == "0.0" || + controller.order.value.endKitoMetersReading!.isEmpty) { + ShowToastDialog.showToast("You are not able to pay now until driver adds kilometer".tr); + } else { + Get.bottomSheet(paymentBottomSheet(context, controller, isDark, controller.order.value), isScrollControlled: true, backgroundColor: Colors.transparent); + } + }, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + ), + ), + if (controller.order.value.status == Constant.orderPlaced || controller.order.value.status == Constant.driverAccepted) + Expanded( + child: RoundedButtonFill( + title: "Cancel Booking", + onPress: () { + controller.cancelRentalRequest(controller.order.value); + }, + color: AppThemeData.danger300, + textColor: AppThemeData.surface, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _summaryTile(String title, String value, bool isDark, Color? colors) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + Text(value, style: AppThemeData.semiBoldTextStyle(fontSize: title == "Order Total" ? 18 : 16, color: colors ?? (isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))), + ], + ), + ); + } + + Widget paymentBottomSheet(BuildContext context, RentalOrderDetailsController controller, bool isDark, RentalOrderModel orderModel) { + return DraggableScrollableSheet( + initialChildSize: 0.70, + // Start height + minChildSize: 0.30, + // Minimum height + maxChildSize: 0.8, + // Maximum height + expand: false, + //Prevents full-screen takeover + builder: (context, scrollController) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + decoration: BoxDecoration(color: isDark ? AppThemeData.grey500 : Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Select Payment Method".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Icon(Icons.close), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + controller: scrollController, + children: [ + Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)), + const SizedBox(height: 10), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility( + visible: controller.walletSettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"), + ), + Visibility( + visible: controller.cashOnDeliverySettingModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"), + ), + ], + ), + ), + ), + if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text( + "Other Payment Options".tr, + textAlign: TextAlign.start, + style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500), + ), + const SizedBox(height: 10), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")), + Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")), + Visibility(visible: controller.payStackModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png")), + Visibility( + visible: controller.mercadoPagoModel.value.isEnabled == true, + child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"), + ), + Visibility( + visible: controller.flutterWaveModel.value.isEnable == true, + child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"), + ), + Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")), + Visibility(visible: controller.razorPayModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png")), + Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")), + Visibility( + visible: controller.orangeMoneyModel.value.enable == true, + child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"), + ), + Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")), + ], + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + RoundedButtonFill( + title: "Continue".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey900, + onPress: () async { + if (controller.selectedPaymentMethod.value.isEmpty) { + ShowToastDialog.showToast("Please select a payment method".tr); + } else { + if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) { + controller.stripeMakePayment(amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) { + controller.paypalPaymentSheet(controller.totalAmount.value.toString(), context); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) { + controller.payStackPayment(controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) { + controller.mercadoPagoMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) { + controller.flutterWaveInitiatePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) { + controller.payFastPayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.completeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) { + if (Constant.userModel!.walletAmount == null || Constant.userModel!.walletAmount! < controller.totalAmount.value) { + ShowToastDialog.showToast("You do not have sufficient wallet balance".tr); + } else { + controller.completeOrder(); + } + } else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) { + controller.completeOrder(); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) { + controller.midtransMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) { + controller.orangeMakePayment(context: context, amount: controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) { + controller.xenditPayment(context, controller.totalAmount.value.toString()); + } else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) { + RazorPayController().createOrderRazorPay(amount: double.parse(controller.totalAmount.value.toString()), razorpayModel: controller.razorPayModel.value).then((value) { + if (value == null) { + Get.back(); + ShowToastDialog.showToast("Something went wrong, please contact admin.".tr); + } else { + CreateRazorPayOrderModel result = value; + controller.openCheckout(amount: controller.totalAmount.value.toString(), orderId: result.id); + } + }); + } else { + ShowToastDialog.showToast("Please select payment method".tr); + } + } + }, + ), + ], + ), + ); + }, + ); + } + + Obx cardDecoration(RentalOrderDetailsController controller, PaymentGateway value, isDark, String image) { + return Obx( + () => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + children: [ + InkWell( + onTap: () { + controller.selectedPaymentMethod.value = value.name; + }, + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))), + child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)), + ), + const SizedBox(width: 10), + value.name == "wallet" + ? Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + Text( + Constant.amountShow(amount: Constant.userModel!.walletAmount == null ? '0.0' : Constant.userModel!.walletAmount.toString()), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300), + ), + ], + ), + ) + : Expanded( + child: Text( + value.name.capitalizeString(), + textAlign: TextAlign.start, + style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900), + ), + ), + const Expanded(child: SizedBox()), + Radio( + value: value.name, + groupValue: controller.selectedPaymentMethod.value, + activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300, + onChanged: (value) { + controller.selectedPaymentMethod.value = value.toString(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screen_ui/rental_service/rental_review_screen.dart b/lib/screen_ui/rental_service/rental_review_screen.dart new file mode 100644 index 0000000..98c00b1 --- /dev/null +++ b/lib/screen_ui/rental_service/rental_review_screen.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:get/get.dart'; +import '../../constant/constant.dart'; +import '../../controllers/rental_review_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../themes/round_button_fill.dart'; +import '../../themes/text_field_widget.dart'; +import '../../utils/network_image_widget.dart'; + +class RentalReviewScreen extends StatelessWidget { + const RentalReviewScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + return GetX( + init: RentalReviewController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + elevation: 0, + backgroundColor: AppThemeData.primary300, + leading: GestureDetector( + onTap: () => Get.back(), + child: Icon(Icons.arrow_back_ios, color: isDark ? Colors.white : Colors.black), + ), + title: Text( + controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, + style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 16), + ), + ), + body: Obx( + () => + controller.isLoading.value + ? Constant.loader() + : Padding( + padding: const EdgeInsets.only(top: 20), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 50, bottom: 20), + child: Card( + elevation: 2, + color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 65), + child: Column( + children: [ + // Driver Name + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + controller.order.value!.driver?.fullName() ?? "", + style: TextStyle( + color: isDark ? Colors.white : Colors.black87, + fontFamily: AppThemeData.medium, + fontSize: 18, + ), + ), + ), + // Car info + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + controller.driverUser.value?.carNumber?.toUpperCase() ?? '', + style: TextStyle( + color: isDark ? Colors.white : Colors.black87, + fontFamily: AppThemeData.medium, + ), + ), + const SizedBox(width: 8), + Text( + "${controller.driverUser.value?.carName} ${controller.driverUser.value?.carMakes}", + style: TextStyle( + color: isDark ? Colors.white : Colors.black38, + fontFamily: AppThemeData.medium, + ), + ), + ], + ), + + const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: Divider(color: Colors.grey)), + + // Title + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + 'How is your trip?'.tr, + style: TextStyle( + fontSize: 18, + color: isDark ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'Your feedback will help us improve \n driving experience better'.tr, + textAlign: TextAlign.center, + style: TextStyle( + color: isDark ? Colors.white : Colors.black.withOpacity(0.60), + letterSpacing: 0.8, + ), + ), + ), + + // Rating + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + 'Rate for'.tr, + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.white : Colors.black.withOpacity(0.60), + letterSpacing: 0.8, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + controller.order.value!.driver?.fullName() ?? "", + style: TextStyle( + fontSize: 18, + color: isDark ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: RatingBar.builder( + initialRating: controller.ratings.value, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemBuilder: (context, _) => const Icon(Icons.star, color: Colors.amber), + unratedColor: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400, + onRatingUpdate: (rating) => controller.ratings.value = rating, + ), + ), + + // Comment + Padding( + padding: const EdgeInsets.all(20.0), + child: TextFieldWidget( + hintText: "Type comment....".tr, + controller: controller.comment.value, + maxLine: 5, + ), + ), + + // Submit Button + Padding( + padding: const EdgeInsets.all(20.0), + child: RoundedButtonFill( + title: controller.ratingModel.value != null ? "Update Review" : "Add Review", + color: AppThemeData.primary300, + textColor: isDark ? Colors.white : Colors.black, + onPress: controller.submitReview, + ), + ), + ], + ), + ), + ), + ), + ), + Align( + alignment: Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.15), blurRadius: 8, spreadRadius: 6)], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(60), + child: NetworkImageWidget( + imageUrl: controller.order.value?.driver?.profilePictureURL ?? '', + fit: BoxFit.cover, + height: 110, + width: 110, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screen_ui/service_home_screen/service_list_screen.dart b/lib/screen_ui/service_home_screen/service_list_screen.dart new file mode 100644 index 0000000..b6a5aaf --- /dev/null +++ b/lib/screen_ui/service_home_screen/service_list_screen.dart @@ -0,0 +1,153 @@ +import 'package:customer/constant/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/service_list_controller.dart'; +import '../../controllers/theme_controller.dart'; +import '../../themes/app_them_data.dart'; +import '../../utils/network_image_widget.dart'; + +class ServiceListScreen extends StatelessWidget { + const ServiceListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + return GetX( + init: ServiceListController(), + builder: (controller) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + titleSpacing: 20, + centerTitle: false, + title: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("eMart".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 22, color: themeController.isDark.value ? AppThemeData.grey50 : AppThemeData.grey900)), + Text("All Your Needs in One App!".tr, style: AppThemeData.regularTextStyle(fontSize: 14, color: themeController.isDark.value ? AppThemeData.grey100 : AppThemeData.grey700)), + ], + ), + ), + body: + controller.isLoading.value + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + controller.serviceListBanner.isEmpty ? SizedBox() : BannerView(bannerList: controller.serviceListBanner), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Explore Our Services".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 20, color: themeController.isDark.value ? AppThemeData.grey50 : AppThemeData.grey900)), + const SizedBox(height: 12), + GridView.builder( + itemCount: controller.sectionList.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, mainAxisSpacing: 12, crossAxisSpacing: 12, mainAxisExtent: 130), + itemBuilder: (context, index) { + final section = controller.sectionList[index]; + return GestureDetector( + onTap: () => controller.onServiceTap(context, section), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient(colors: Constant.sectionColor[index % Constant.sectionColor.length], begin: Alignment.topCenter, end: Alignment.bottomCenter), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, left: 5, right: 5), + child: Text( + section.name ?? '', + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: AppThemeData.grey900), + ), + ), + const Spacer(), + NetworkImageWidget(imageUrl: section.sectionImage ?? '', width: 80, height: 60, fit: BoxFit.contain), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class BannerView extends StatelessWidget { + final List bannerList; + final RxInt currentPage = 0.obs; + final ScrollController scrollController = ScrollController(); + + BannerView({super.key, required this.bannerList}); + + void onScroll(BuildContext context) { + if (scrollController.hasClients && bannerList.isNotEmpty) { + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = screenWidth * 0.8 + 10; // 80% width + spacing + final offset = scrollController.offset; + final index = (offset / itemWidth).round(); + + if (index != currentPage.value && index < bannerList.length) { + currentPage.value = index; + } + } + } + + @override + Widget build(BuildContext context) { + scrollController.addListener(() => onScroll(context)); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + SizedBox( + height: 168, + child: ListView.separated( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: bannerList.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox(width: MediaQuery.of(context).size.width * 0.8, child: NetworkImageWidget(imageUrl: bannerList[index].toString(), fit: BoxFit.fill)), + ); + }, + ), + ), + const SizedBox(height: 8), + Obx(() { + return Row( + children: List.generate(bannerList.length, (index) { + final isSelected = currentPage.value == index; + return Expanded(child: Container(height: 4, decoration: BoxDecoration(color: isSelected ? AppThemeData.grey300 : AppThemeData.grey100, borderRadius: BorderRadius.circular(5)))); + }), + ); + }), + ], + ), + ); + } +} diff --git a/lib/screen_ui/splash_screen/splash_screen.dart b/lib/screen_ui/splash_screen/splash_screen.dart new file mode 100644 index 0000000..652d0e0 --- /dev/null +++ b/lib/screen_ui/splash_screen/splash_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constant/assets.dart'; +import '../../controllers/splash_controller.dart'; +import '../../themes/app_them_data.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: SplashController(), + builder: (controller) { + return Scaffold(backgroundColor: AppThemeData.surface, body: Center(child: Image.asset(AppAssets.icAppLogo, width: 120, height: 120))); + }, + ); + } +} diff --git a/lib/service/cart_provider.dart b/lib/service/cart_provider.dart new file mode 100644 index 0000000..4853938 --- /dev/null +++ b/lib/service/cart_provider.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:customer/themes/custom_dialog_box.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'database_helper.dart'; + +class CartProvider with ChangeNotifier { + final _cartStreamController = StreamController>.broadcast(); + List _cartItems = []; + + Stream> get cartStream => _cartStreamController.stream; + + CartProvider() { + _initCart(); + } + + Future _initCart() async { + _cartItems = await DatabaseHelper.instance.fetchCartProducts(); + _cartStreamController.sink.add(_cartItems); + } + + Future addToCart(BuildContext context, CartProductModel product, int quantity) async { + _cartItems = await DatabaseHelper.instance.fetchCartProducts(); + if ((_cartItems.where((item) => item.id == product.id)).isNotEmpty) { + var index = _cartItems.indexWhere((item) => item.id == product.id); + _cartItems[index].quantity = quantity; + if (product.extras != null || product.extras!.isNotEmpty) { + _cartItems[index].extras = product.extras; + _cartItems[index].extrasPrice = product.extrasPrice; + } else { + _cartItems[index].extras = []; + _cartItems[index].extrasPrice = "0"; + } + await DatabaseHelper.instance.updateCartProduct(_cartItems[index]); + } else { + if (_cartItems.isEmpty || _cartItems.where((item) => item.vendorID == product.vendorID).isNotEmpty) { + product.quantity = quantity; + _cartItems.add(product); + cartItem.add(product); + await DatabaseHelper.instance.insertCartProduct(product); + log("===> insert"); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return CustomDialogBox( + title: "Alert".tr, + descriptions: "Your cart already contains items from another restaurant. Would you like to replace them with items from this restaurant instead?".tr, + positiveString: "Add".tr, + negativeString: "Cancel".tr, + positiveClick: () async { + cartItem.clear(); + _cartItems.clear(); + DatabaseHelper.instance.deleteAllCartProducts(); + addToCart(context, product, quantity); + Get.back(); + }, + negativeClick: () { + Get.back(); + }, + img: null, + ); + }, + ); + } + } + _initCart(); + } + + Future removeFromCart(CartProductModel product, int quantity) async { + _cartItems = await DatabaseHelper.instance.fetchCartProducts(); + var index = _cartItems.indexWhere((item) => item.id == product.id); + if (index >= 0) { + _cartItems[index].quantity = quantity; + if (_cartItems[index].quantity == 0) { + await DatabaseHelper.instance.deleteCartProduct(product.id!); + _cartItems.removeAt(index); + cartItem.removeAt(index); + } else { + await DatabaseHelper.instance.updateCartProduct(_cartItems[index]); + } + } + _initCart(); + } + + Future clearDatabase() async { + _cartItems.clear(); + cartItem.clear(); + _cartStreamController.sink.add(_cartItems); + } +} diff --git a/lib/service/database_helper.dart b/lib/service/database_helper.dart new file mode 100644 index 0000000..8140162 --- /dev/null +++ b/lib/service/database_helper.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/cart_product_model.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +class DatabaseHelper { + static final DatabaseHelper instance = DatabaseHelper._init(); + static Database? _database; + + DatabaseHelper._init(); + + Future get database async { + if (_database != null) return _database!; + _database = await _initDB('cart.db'); + return _database!; + } + + Future _initDB(String filePath) async { + final dbPath = await getDatabasesPath(); + final path = join(dbPath, filePath); + + return await openDatabase( + path, + version: 1, + onCreate: _createDB, + ); + } + + Future _createDB(Database db, int version) async { + // const idType = 'TEXT PRIMARY KEY'; + const textType = 'TEXT NOT NULL'; + const intType = 'INTEGER NOT NULL'; + + await db.execute(''' + CREATE TABLE cart_products ( + id $textType, + category_id $textType, + name $textType, + photo $textType, + price $textType, + discountPrice $textType, + vendorID $textType, + quantity $intType, + extras_price $textType, + extras $textType, + variant_info $textType NULL + ) + '''); + print('Table cart_products created'); // Debugging + } + + Future insertCartProduct(CartProductModel product) async { + log(product.toJson().toString()); + final db = await instance.database; + await db.insert( + 'cart_products', + product.toJson() + ..['variant_info'] = jsonEncode(product.variantInfo) + ..['extras'] = jsonEncode(product.extras), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future> fetchCartProducts() async { + final db = await instance.database; + final maps = await db.query('cart_products'); + return List.generate(maps.length, (i) { + return CartProductModel.fromJson(maps[i]); + }); + } + + Future updateCartProduct(CartProductModel product) async { + log(product.toJson().toString()); + final db = await instance.database; + await db.update( + 'cart_products', + product.toJson() + ..['variant_info'] = jsonEncode(product.variantInfo) + ..['extras'] = jsonEncode(product.extras), + where: 'id = ?', + whereArgs: [product.id], + ); + } + + Future deleteCartProduct(String id) async { + final db = await instance.database; + await db.delete( + 'cart_products', + where: 'id = ?', + whereArgs: [id], + ); + } + + Future close() async { + final db = await instance.database; + db.close(); + } + + Future deleteAllCartProducts() async { + final db = await database; + cartItem.clear(); + await db.delete('cart_products'); + } +} diff --git a/lib/service/fire_store_utils.dart b/lib/service/fire_store_utils.dart new file mode 100644 index 0000000..8c56d2f --- /dev/null +++ b/lib/service/fire_store_utils.dart @@ -0,0 +1,3678 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/constant/collection_name.dart'; +import 'package:customer/models/brands_model.dart'; +import 'package:customer/models/rental_order_model.dart'; +import 'package:customer/models/rental_package_model.dart'; +import 'package:customer/models/user_model.dart'; +import 'package:customer/models/zone_model.dart'; +import 'package:firebase_auth/firebase_auth.dart' as auth; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:uuid/uuid.dart'; +import 'package:video_compress/video_compress.dart'; +import '../constant/constant.dart'; +import '../models/attributes_model.dart'; +import '../models/cab_order_model.dart'; +import '../models/cashback_redeem_model.dart'; +import '../models/category_model.dart'; +import '../models/coupon_model.dart'; +import '../models/currency_model.dart'; +import '../models/favorite_ondemand_service_model.dart'; +import '../models/gift_cards_model.dart'; +import '../models/advertisement_model.dart'; +import '../models/banner_model.dart'; +import '../models/cashback_model.dart'; +import '../models/conversation_model.dart'; +import '../models/dine_in_booking_model.dart'; +import '../models/email_template_model.dart'; +import '../models/favourite_item_model.dart'; +import '../models/favourite_model.dart'; +import '../models/gift_cards_order_model.dart'; +import '../models/inbox_model.dart'; +import '../models/mail_setting.dart'; +import '../models/notification_model.dart'; +import '../models/on_boarding_model.dart'; +import '../models/onprovider_order_model.dart'; +import '../models/order_model.dart'; +import '../models/parcel_category.dart'; +import '../models/parcel_order_model.dart'; +import '../models/parcel_weight_model.dart'; +import '../models/payment_model/cod_setting_model.dart'; +import '../models/payment_model/flutter_wave_model.dart'; +import '../models/payment_model/mercado_pago_model.dart'; +import '../models/payment_model/mid_trans.dart'; +import '../models/payment_model/orange_money.dart'; +import '../models/payment_model/pay_fast_model.dart'; +import '../models/payment_model/pay_stack_model.dart'; +import '../models/payment_model/paypal_model.dart'; +import '../models/payment_model/paytm_model.dart'; +import '../models/payment_model/razorpay_model.dart'; +import '../models/payment_model/stripe_model.dart'; +import '../models/payment_model/wallet_setting_model.dart'; +import '../models/payment_model/xendit.dart'; +import '../models/popular_destination.dart'; +import '../models/product_model.dart'; +import '../models/provider_serivce_model.dart'; +import '../models/rating_model.dart'; +import '../models/referral_model.dart'; +import '../models/rental_vehicle_type.dart'; +import '../models/review_attribute_model.dart'; +import '../models/section_model.dart'; +import '../models/story_model.dart'; +import '../models/tax_model.dart'; +import '../models/vehicle_type.dart'; +import '../models/vendor_category_model.dart'; +import '../models/vendor_model.dart'; +import '../models/wallet_transaction_model.dart'; +import '../models/worker_model.dart'; +import '../screen_ui/multi_vendor_service/chat_screens/ChatVideoContainer.dart'; +import '../themes/app_them_data.dart'; +import '../themes/show_toast_dialog.dart'; +import '../utils/preferences.dart'; +import '../widget/geoflutterfire/src/geoflutterfire.dart'; +import '../widget/geoflutterfire/src/models/point.dart'; + +class FireStoreUtils { + static FirebaseFirestore fireStore = FirebaseFirestore.instance; + + static String getCurrentUid() { + return auth.FirebaseAuth.instance.currentUser!.uid; + } + + static Future isLogin() async { + bool isLogin = false; + if (auth.FirebaseAuth.instance.currentUser != null) { + isLogin = await userExistOrNot( + auth.FirebaseAuth.instance.currentUser!.uid, + ); + } else { + isLogin = false; + } + return isLogin; + } + + static Future userExistOrNot(String uid) async { + bool isExist = false; + + await fireStore + .collection(CollectionName.users) + .doc(uid) + .get() + .then((value) { + if (value.exists) { + isExist = true; + } else { + isExist = false; + } + }) + .catchError((error) { + log("Failed to check user exist: $error"); + isExist = false; + }); + return isExist; + } + + static Future getUserProfile(String uuid) async { + UserModel? userModel; + await fireStore + .collection(CollectionName.users) + .doc(uuid) + .get() + .then((value) { + if (value.exists) { + userModel = UserModel.fromJson(value.data()!); + } + }) + .catchError((error) { + log("Failed to update user: $error"); + userModel = null; + }); + return userModel; + } + + static Future updateUser(UserModel userModel) async { + bool isUpdate = false; + await fireStore + .collection(CollectionName.users) + .doc(userModel.id) + .set(userModel.toJson()) + .whenComplete(() { + Constant.userModel = userModel; + isUpdate = true; + }) + .catchError((error) { + log("Failed to update user: $error"); + isUpdate = false; + }); + return isUpdate; + } + + static Future> getOnBoardingList() async { + List onBoardingModel = []; + await fireStore + .collection(CollectionName.onBoarding) + .where("type", isEqualTo: "customer") + .get() + .then((value) { + for (var element in value.docs) { + OnBoardingModel documentModel = OnBoardingModel.fromJson( + element.data(), + ); + onBoardingModel.add(documentModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return onBoardingModel; + } + + static Future?> getZone() async { + List airPortList = []; + await fireStore + .collection(CollectionName.zone) + .where('publish', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + ZoneModel ariPortModel = ZoneModel.fromJson(element.data()); + airPortList.add(ariPortModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return airPortList; + } + + static Future referralAdd(ReferralModel ratingModel) async { + try { + await fireStore + .collection(CollectionName.referral) + .doc(ratingModel.id) + .set(ratingModel.toJson()); + } catch (e, s) { + print('FireStoreUtils.referralAdd $e $s'); + return "Couldn't review".tr; + } + return null; + } + + static Future getReferralUserByCode( + String referralCode, + ) async { + ReferralModel? referralModel; + try { + await fireStore + .collection(CollectionName.referral) + .where("referralCode", isEqualTo: referralCode) + .get() + .then((value) { + if (value.docs.isNotEmpty) { + referralModel = ReferralModel.fromJson(value.docs.first.data()); + } + }); + } catch (e, s) { + print('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return referralModel; + } + + static Future> getSections() async { + List sections = []; + QuerySnapshot> productsQuery = + await fireStore + .collection(CollectionName.sections) + .where("isActive", isEqualTo: true) + .get(); + + await Future.forEach(productsQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + try { + sections.add(SectionModel.fromJson(document.data())); + } catch (e) { + print('**-FireStoreUtils.getSection Parse error $e'); + } + }); + return sections; + } + + static Future> getSectionBannerList() async { + List sections = []; + await fireStore + .collection(CollectionName.settings) + .doc("AppHomeBanners") + .get() + .then((value) { + if (value.exists) { + sections = value.data()!['banners'] ?? []; + } + }); + return sections; + } + + static Future getCurrency() async { + CurrencyModel? currency; + await fireStore + .collection(CollectionName.currency) + .where("isActive", isEqualTo: true) + .get() + .then((value) { + if (value.docs.isNotEmpty) { + currency = CurrencyModel.fromJson(value.docs.first.data()); + } + }); + return currency; + } + + static Future> getAllAdvertisement() async { + List advertisementList = []; + await fireStore + .collection(CollectionName.advertisements) + .where('status', isEqualTo: 'approved') + .where('paymentStatus', isEqualTo: true) + .where('startDate', isLessThanOrEqualTo: DateTime.now()) + .where('endDate', isGreaterThan: DateTime.now()) + .orderBy('priority', descending: false) + .get() + .then((value) { + for (var element in value.docs) { + AdvertisementModel advertisementModel = AdvertisementModel.fromJson( + element.data(), + ); + if (advertisementModel.isPaused == null || + advertisementModel.isPaused == false) { + advertisementList.add(advertisementModel); + } + } + }); + return advertisementList; + } + + static Future> getFavouriteRestaurant() async { + List favouriteList = []; + await fireStore + .collection(CollectionName.favoriteVendor) + .where('user_id', isEqualTo: getCurrentUid()) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .get() + .then((value) { + for (var element in value.docs) { + FavouriteModel favouriteModel = FavouriteModel.fromJson( + element.data(), + ); + favouriteList.add(favouriteModel); + } + }); + log("CollectionName.favoriteRestaurant :: ${favouriteList.length}"); + return favouriteList; + } + + static Future getEmailTemplates(String type) async { + EmailTemplateModel? emailTemplateModel; + await fireStore + .collection(CollectionName.emailTemplates) + .where('type', isEqualTo: type) + .get() + .then((value) { + print("------>"); + if (value.docs.isNotEmpty) { + print(value.docs.first.data()); + emailTemplateModel = EmailTemplateModel.fromJson( + value.docs.first.data(), + ); + } + }); + return emailTemplateModel; + } + + static Future> getCashbackList() async { + List cashbackList = []; + try { + await fireStore + .collection(CollectionName.cashback) + .where('isEnabled', isEqualTo: true) + .where('startDate', isLessThanOrEqualTo: Timestamp.now()) + .where('endDate', isGreaterThanOrEqualTo: Timestamp.now()) + .get() + .then((event) { + if (event.docs.isNotEmpty) { + for (var element in event.docs) { + CashbackModel cashbackModel = CashbackModel.fromJson( + element.data(), + ); + if (cashbackModel.customerIds == null || + cashbackModel.customerIds?.contains( + FireStoreUtils.getCurrentUid(), + ) == + true) { + cashbackList.add(cashbackModel); + } + } + } + }); + } catch (error, stackTrace) { + log( + 'Error fetching redeemed cashback data: $error', + stackTrace: stackTrace, + ); + } + + return cashbackList; + } + + static Future addDriverInbox(InboxModel inboxModel) async { + return await fireStore + .collection("chat_driver") + .doc(inboxModel.orderId) + .set(inboxModel.toJson()) + .then((document) { + return inboxModel; + }); + } + + static Future addDriverChat(ConversationModel conversationModel) async { + return await fireStore + .collection("chat_driver") + .doc(conversationModel.orderId) + .collection("thread") + .doc(conversationModel.id) + .set(conversationModel.toJson()) + .then((document) { + return conversationModel; + }); + } + + static Future addRestaurantInbox(InboxModel inboxModel) async { + return await fireStore + .collection("chat_store") + .doc(inboxModel.orderId) + .set(inboxModel.toJson()) + .then((document) { + return inboxModel; + }); + } + + static Future addRestaurantChat(ConversationModel conversationModel) async { + return await fireStore + .collection("chat_store") + .doc(conversationModel.orderId) + .collection("thread") + .doc(conversationModel.id) + .set(conversationModel.toJson()) + .then((document) { + return conversationModel; + }); + } + + static Future addWorkerInbox(InboxModel inboxModel) async { + return await fireStore + .collection("chat_worker") + .doc(inboxModel.orderId) + .set(inboxModel.toJson()) + .then((document) { + return inboxModel; + }); + } + + static Future addWorkerChat(ConversationModel conversationModel) async { + return await fireStore + .collection("chat_worker") + .doc(conversationModel.orderId) + .collection("thread") + .doc(conversationModel.id) + .set(conversationModel.toJson()) + .then((document) { + return conversationModel; + }); + } + + static Future addProviderInbox(InboxModel inboxModel) async { + return await fireStore + .collection("chat_provider") + .doc(inboxModel.orderId) + .set(inboxModel.toJson()) + .then((document) { + return inboxModel; + }); + } + + static Future addProviderChat(ConversationModel conversationModel) async { + return await fireStore + .collection("chat_provider") + .doc(conversationModel.orderId) + .collection("thread") + .doc(conversationModel.id) + .set(conversationModel.toJson()) + .then((document) { + return conversationModel; + }); + } + + static Future?> getTaxList(String? sectionId) async { + List taxList = []; + List placeMarks = await placemarkFromCoordinates( + Constant.selectedLocation.location!.latitude ?? 0.0, + Constant.selectedLocation.location!.longitude ?? 0.0, + ); + await fireStore + .collection(CollectionName.tax) + .where('sectionId', isEqualTo: sectionId) + .where('country', isEqualTo: placeMarks.first.country) + .where('enable', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + TaxModel taxModel = TaxModel.fromJson(element.data()); + taxList.add(taxModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + + return taxList; + } + + static Future> getDineInBooking( + bool isUpcoming, + ) async { + List list = []; + + if (isUpcoming) { + await fireStore + .collection(CollectionName.bookedTable) + .where('authorID', isEqualTo: getCurrentUid()) + .where('date', isGreaterThan: Timestamp.now()) + .orderBy('date', descending: true) + .orderBy('createdAt', descending: true) + .get() + .then((value) { + for (var element in value.docs) { + DineInBookingModel taxModel = DineInBookingModel.fromJson( + element.data(), + ); + list.add(taxModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + } else { + await fireStore + .collection(CollectionName.bookedTable) + .where('authorID', isEqualTo: getCurrentUid()) + .where('date', isLessThan: Timestamp.now()) + .orderBy('date', descending: true) + .orderBy('createdAt', descending: true) + .get() + .then((value) { + for (var element in value.docs) { + DineInBookingModel taxModel = DineInBookingModel.fromJson( + element.data(), + ); + list.add(taxModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + } + + return list; + } + + static Future> getHomeVendorCategory() async { + List list = []; + await fireStore + .collection(CollectionName.vendorCategories) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .where("show_in_homepage", isEqualTo: true) + .where('publish', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + VendorCategoryModel walletTransactionModel = + VendorCategoryModel.fromJson(element.data()); + list.add(walletTransactionModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return list; + } + + static Future> getProductListByBrandId( + String brandId, + ) async { + List list = []; + await fireStore + .collection(CollectionName.vendorProducts) + .where('brandID', isEqualTo: brandId) + .where('publish', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + ProductModel walletTransactionModel = ProductModel.fromJson( + element.data(), + ); + list.add(walletTransactionModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return list; + } + + static Future> getHomeBottomBanner() async { + List bannerList = []; + await fireStore + .collection(CollectionName.bannerItems) + .where("is_publish", isEqualTo: true) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where("position", isEqualTo: "middle") + .orderBy("set_order", descending: false) + .get() + .then((value) { + for (var element in value.docs) { + BannerModel bannerHome = BannerModel.fromJson(element.data()); + bannerList.add(bannerHome); + } + }); + return bannerList; + } + + static Future> getBrandList() async { + List brandList = []; + await fireStore + .collection(CollectionName.brands) + .where("is_publish", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + BrandsModel bannerHome = BrandsModel.fromJson(element.data()); + brandList.add(bannerHome); + } + }); + return brandList; + } + + static Future setBookedOrder(DineInBookingModel orderModel) async { + bool isAdded = false; + await fireStore + .collection(CollectionName.bookedTable) + .doc(orderModel.id) + .set(orderModel.toJson()) + .then((value) { + isAdded = true; + }) + .catchError((error) { + log("Failed to update user: $error"); + isAdded = false; + }); + return isAdded; + } + + static Future getVendorCuisines(String id) async { + List tagList = []; + List prodTagList = []; + QuerySnapshot> productsQuery = + await fireStore + .collection(CollectionName.vendorProducts) + .where('vendorID', isEqualTo: id) + .get(); + await Future.forEach(productsQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + if (document.data().containsKey("categoryID") && + document.data()['categoryID'].toString().isNotEmpty) { + prodTagList.add(document.data()['categoryID']); + } + }); + QuerySnapshot> catQuery = + await fireStore + .collection(CollectionName.vendorCategories) + .where('publish', isEqualTo: true) + .get(); + await Future.forEach(catQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + Map catDoc = document.data(); + if (catDoc.containsKey("id") && + catDoc['id'].toString().isNotEmpty && + catDoc.containsKey("title") && + catDoc['title'].toString().isNotEmpty && + prodTagList.contains(catDoc['id'])) { + tagList.add(catDoc['title']); + } + }); + return tagList; + } + + static Future> getFavouriteItem() async { + List favouriteList = []; + await fireStore + .collection(CollectionName.favoriteItem) + .where('user_id', isEqualTo: getCurrentUid()) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .get() + .then((value) { + for (var element in value.docs) { + FavouriteItemModel favouriteModel = FavouriteItemModel.fromJson( + element.data(), + ); + favouriteList.add(favouriteModel); + } + }); + return favouriteList; + } + + static Future getVendorById(String vendorId) async { + VendorModel? vendorModel; + try { + await fireStore + .collection(CollectionName.vendors) + .doc(vendorId) + .get() + .then((value) { + if (value.exists) { + vendorModel = VendorModel.fromJson(value.data()!); + } + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return vendorModel; + } + + static Future getProductById(String productId) async { + ProductModel? vendorCategoryModel; + try { + await fireStore + .collection(CollectionName.vendorProducts) + .doc(productId) + .get() + .then((value) { + if (value.exists) { + vendorCategoryModel = ProductModel.fromJson(value.data()!); + } + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return vendorCategoryModel; + } + + static Future> getGiftCard() async { + List giftCardModelList = []; + QuerySnapshot> currencyQuery = + await fireStore + .collection(CollectionName.giftCards) + .where("isEnable", isEqualTo: true) + .get(); + await Future.forEach(currencyQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + try { + log(document.data().toString()); + giftCardModelList.add(GiftCardsModel.fromJson(document.data())); + } catch (e) { + debugPrint('FireStoreUtils.get Currency Parse error $e'); + } + }); + return giftCardModelList; + } + + static Future setWalletTransaction( + WalletTransactionModel walletTransactionModel, + ) async { + bool isAdded = false; + await fireStore + .collection(CollectionName.wallet) + .doc(walletTransactionModel.id) + .set(walletTransactionModel.toJson()) + .then((value) { + isAdded = true; + }) + .catchError((error) { + log("Failed to update user: $error"); + isAdded = false; + }); + return isAdded; + } + + static Future getSettings() async { + try { + final restaurantSnap = + await FirebaseFirestore.instance + .collection(CollectionName.settings) + .doc('vendor') + .get(); + + if (restaurantSnap.exists && restaurantSnap.data() != null) { + Constant.isSubscriptionModelApplied = + restaurantSnap.data()?['subscription_model'] ?? false; + } else { + Constant.isSubscriptionModelApplied = false; + } + + fireStore + .collection(CollectionName.settings) + .doc("DriverNearBy") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.distanceType = event.data()?["distanceType"] ?? "km"; + Constant.isEnableOTPTripStart = + event.data()?["enableOTPTripStart"] ?? false; + Constant.isEnableOTPTripStartForRental = + event.data()?["enableOTPTripStartForRental"] ?? false; + } + }); + + fireStore + .collection(CollectionName.settings) + .doc("maintenance_settings") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.isMaintenanceModeForCustomer = + event.data()?["isMaintenanceModeForCustomer"] ?? false; + } + }); + + final globalSettingsSnap = + await FirebaseFirestore.instance + .collection(CollectionName.settings) + .doc("globalSettings") + .get(); + + if (globalSettingsSnap.exists && globalSettingsSnap.data() != null) { + Constant.isEnableAdsFeature = + globalSettingsSnap.data()?['isEnableAdsFeature'] ?? false; + Constant.isSelfDeliveryFeature = + globalSettingsSnap.data()?['isSelfDelivery'] ?? false; + Constant.defaultCountryCode = + globalSettingsSnap.data()?['defaultCountryCode'] ?? ''; + Constant.defaultCountry = + globalSettingsSnap.data()?['defaultCountry'] ?? ''; + + String? colorStr = globalSettingsSnap.data()?['app_customer_color']; + if (colorStr != null && colorStr.isNotEmpty) { + AppThemeData.primary300 = Color( + int.parse(colorStr.replaceFirst("#", "0xff")), + ); + } + } + + fireStore + .collection(CollectionName.settings) + .doc("googleMapKey") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.mapAPIKey = event.data()?["key"] ?? ""; + } + }); + fireStore + .collection(CollectionName.settings) + .doc("placeHolderImage") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.placeHolderImage = event.data()?["image"] ?? ""; + } + }); + + fireStore + .collection(CollectionName.settings) + .doc("notification_setting") + .snapshots() + .listen((event) { + if (event.exists) { + Constant.senderId = event.data()?["senderId"]; + Constant.jsonNotificationFileURL = event.data()?["serviceJson"]; + } + }); + + final cashbackSnap = + await fireStore + .collection(CollectionName.settings) + .doc("cashbackOffer") + .get(); + + if (cashbackSnap.exists && cashbackSnap.data() != null) { + Constant.isCashbackActive = cashbackSnap.data()?["isEnable"] ?? false; + } else { + Constant.isCashbackActive = false; + } + + final driverNearBySnap = + await fireStore + .collection(CollectionName.settings) + .doc("DriverNearBy") + .get(); + + if (driverNearBySnap.exists && driverNearBySnap.data() != null) { + Constant.selectedMapType = + driverNearBySnap.data()?["selectedMapType"] ?? ""; + Constant.mapType = driverNearBySnap.data()?["mapType"] ?? ""; + } + + fireStore + .collection(CollectionName.settings) + .doc("privacyPolicy") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.privacyPolicy = event.data()?["privacy_policy"] ?? ""; + } + }); + + fireStore + .collection(CollectionName.settings) + .doc("termsAndConditions") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.termsAndConditions = + event.data()?["termsAndConditions"] ?? ""; + } + }); + + fireStore + .collection(CollectionName.settings) + .doc("walletSettings") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.walletSetting = event.data()?["isEnabled"] ?? false; + } + }); + + fireStore + .collection(CollectionName.settings) + .doc("Version") + .snapshots() + .listen((event) { + if (event.exists && event.data() != null) { + Constant.googlePlayLink = event.data()?["googlePlayLink"] ?? ''; + Constant.appStoreLink = event.data()?["appStoreLink"] ?? ''; + Constant.appVersion = event.data()?["app_version"] ?? ''; + Constant.websiteUrl = event.data()?["websiteUrl"] ?? ''; + } + }); + + final storySnap = + await fireStore + .collection(CollectionName.settings) + .doc('story') + .get(); + + if (storySnap.exists && storySnap.data() != null) { + Constant.storyEnable = storySnap.data()?['isEnabled'] ?? false; + } else { + Constant.storyEnable = false; + } + + final emailSnap = + await fireStore + .collection(CollectionName.settings) + .doc("emailSetting") + .get(); + + if (emailSnap.exists && emailSnap.data() != null) { + Constant.mailSettings = MailSettings.fromJson(emailSnap.data()!); + } + + final specialDiscountSnap = + await fireStore + .collection(CollectionName.settings) + .doc("specialDiscountOffer") + .get(); + + if (specialDiscountSnap.exists && specialDiscountSnap.data() != null) { + Constant.specialDiscountOffer = + specialDiscountSnap.data()?["isEnable"] ?? false; + } else { + Constant.specialDiscountOffer = false; + } + } catch (e) { + log("getSettings() Error: $e"); + } + } + + static Future> getGiftHistory() async { + List giftCardsOrderList = []; + await fireStore + .collection(CollectionName.giftPurchases) + .where("userid", isEqualTo: FireStoreUtils.getCurrentUid()) + .get() + .then((value) { + for (var element in value.docs) { + GiftCardsOrderModel giftCardsOrderModel = + GiftCardsOrderModel.fromJson(element.data()); + giftCardsOrderList.add(giftCardsOrderModel); + } + }); + return giftCardsOrderList; + } + + static Future> getAllOrder() async { + List list = []; + + print("Current UID: ${getCurrentUid()}"); + print("Section ID: ${Constant.sectionConstantModel?.id}"); + + try { + final snapshot = + await fireStore + .collection(CollectionName.vendorOrders) + .where("authorID", isEqualTo: getCurrentUid()) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .orderBy("createdAt", descending: true) + .get(); + + print("Snapshot size: ${snapshot.docs.length}"); + + for (var element in snapshot.docs) { + OrderModel order = OrderModel.fromJson(element.data()); + print("Order fetched: ${order.id}"); // or other fields + list.add(order); + } + + print("Total Orders added to list: ${list.length}"); + } catch (e) { + print("Error fetching orders: $e"); + } + + return list; + } + + static Future getOrderReviewsByID( + String orderId, + String productID, + ) async { + RatingModel? ratingModel; + + await fireStore + .collection(CollectionName.itemsReview) + .where('orderid', isEqualTo: orderId) + .where('productId', isEqualTo: productID) + .get() + .then((value) { + if (value.docs.isNotEmpty) { + ratingModel = RatingModel.fromJson(value.docs.first.data()); + } + }) + .catchError((error) { + log(error.toString()); + }); + return ratingModel; + } + + static Future getVendorCategoryByCategoryId( + String categoryId, + ) async { + VendorCategoryModel? vendorCategoryModel; + try { + await fireStore + .collection(CollectionName.vendorCategories) + .doc(categoryId) + .get() + .then((value) { + if (value.exists) { + vendorCategoryModel = VendorCategoryModel.fromJson(value.data()!); + } + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return vendorCategoryModel; + } + + static Future getVendorReviewAttribute( + String attributeId, + ) async { + ReviewAttributeModel? vendorCategoryModel; + try { + await fireStore + .collection(CollectionName.reviewAttributes) + .doc(attributeId) + .get() + .then((value) { + if (value.exists) { + vendorCategoryModel = ReviewAttributeModel.fromJson( + value.data()!, + ); + } + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return vendorCategoryModel; + } + + // static Future setRatingModel(RatingModel ratingModel) async { + // bool isAdded = false; + // await fireStore + // .collection(CollectionName.itemsReview) + // .doc(ratingModel.id) + // .set(ratingModel.toJson()) + // .then((value) { + // isAdded = true; + // }) + // .catchError((error) { + // log("Failed to update user: $error"); + // isAdded = false; + // }); + // return isAdded; + // } + + static Future updateVendor(VendorModel vendor) async { + return await fireStore + .collection(CollectionName.vendors) + .doc(vendor.id) + .set(vendor.toJson()) + .then((document) { + return vendor; + }); + } + + static Future setProduct(ProductModel orderModel) async { + bool isAdded = false; + await fireStore + .collection(CollectionName.vendorProducts) + .doc(orderModel.id) + .set(orderModel.toJson()) + .then((value) { + isAdded = true; + }) + .catchError((error) { + log("Failed to update user: $error"); + isAdded = false; + }); + return isAdded; + } + + static Future getReferralUserBy() async { + ReferralModel? referralModel; + try { + await fireStore + .collection(CollectionName.referral) + .doc(getCurrentUid()) + .get() + .then((value) { + referralModel = ReferralModel.fromJson(value.data()!); + }); + } catch (e, s) { + print('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return referralModel; + } + + static Future> getProductByVendorId( + String vendorId, + ) async { + String selectedFoodType = Preferences.getString( + Preferences.foodDeliveryType, + defaultValue: "Delivery", + ); + List list = []; + log("GetProductByVendorId :: $selectedFoodType"); + if (selectedFoodType == "TakeAway") { + await fireStore + .collection(CollectionName.vendorProducts) + .where("vendorID", isEqualTo: vendorId) + .where('publish', isEqualTo: true) + .orderBy("createdAt", descending: false) + .get() + .then((value) { + for (var element in value.docs) { + ProductModel productModel = ProductModel.fromJson(element.data()); + list.add(productModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + } else { + await fireStore + .collection(CollectionName.vendorProducts) + .where("vendorID", isEqualTo: vendorId) + .where("takeawayOption", isEqualTo: false) + .where('publish', isEqualTo: true) + .orderBy("createdAt", descending: false) + .get() + .then((value) { + for (var element in value.docs) { + ProductModel productModel = ProductModel.fromJson(element.data()); + list.add(productModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + } + + return list; + } + + static Future getDeliveryCharge() async { + DeliveryCharge? deliveryCharge; + try { + await fireStore + .collection(CollectionName.settings) + .doc("DeliveryCharge") + .get() + .then((value) { + if (value.exists) { + deliveryCharge = DeliveryCharge.fromJson(value.data()!); + } + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return deliveryCharge; + } + + static Future> getAllVendorPublicCoupons( + String vendorId, + ) async { + List coupon = []; + + await fireStore + .collection(CollectionName.coupons) + .where("vendorID", isEqualTo: vendorId) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .where("isEnabled", isEqualTo: true) + .where("isPublic", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel taxModel = CouponModel.fromJson(element.data()); + coupon.add(taxModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + print("coupon :::::::::::::::::${coupon.length}"); + return coupon; + } + + static Future> getAllVendorCoupons(String vendorId) async { + List coupon = []; + + await fireStore + .collection(CollectionName.coupons) + .where("vendorID", isEqualTo: vendorId) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .where("isEnabled", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel taxModel = CouponModel.fromJson(element.data()); + coupon.add(taxModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + print("coupon :::::::::::::::::${coupon.length}"); + return coupon; + } + + static Future> getAllCashbak() async { + List cashbackList = []; + await fireStore + .collection(CollectionName.cashback) + .get() + .then((value) { + cashbackList = + value.docs.map((doc) { + return CashbackModel.fromJson(doc.data()); + }).toList(); + }) + .catchError((error) { + log(error.toString()); + }); + + return cashbackList; + } + + static Future> getRedeemedCashbacks( + String cashbackId, + ) async { + List redeemedDocs = []; + + try { + await fireStore + .collection(CollectionName.cashbackRedeem) + .where('userId', isEqualTo: FireStoreUtils.getCurrentUid()) + .where('cashbackId', isEqualTo: cashbackId) + .get() + .then((value) { + redeemedDocs = + value.docs.map((doc) { + return CashbackRedeemModel.fromJson(doc.data()); + }).toList(); + }); + } catch (error, stackTrace) { + log( + 'Error fetching redeemed cashback data: $error', + stackTrace: stackTrace, + ); + } + + return redeemedDocs; + } + + static Future setCashbackRedeemModel( + CashbackRedeemModel cashbackRedeemModel, + ) async { + bool isAdded = false; + await fireStore + .collection(CollectionName.cashbackRedeem) + .doc(cashbackRedeemModel.id) + .set(cashbackRedeemModel.toJson()) + .then((value) { + isAdded = true; + }) + .catchError((error) { + log("Failed to update user: $error"); + isAdded = false; + }); + return isAdded; + } + + static Future setOrder(OrderModel orderModel) async { + bool isAdded = false; + await fireStore + .collection(CollectionName.vendorOrders) + .doc(orderModel.id) + .set(orderModel.toJson()) + .then((value) { + isAdded = true; + }) + .catchError((error) { + log("Failed to update user: $error"); + isAdded = false; + }); + return isAdded; + } + + static Future> getOfferByVendorId(String vendorId) async { + List couponList = []; + await fireStore + .collection(CollectionName.coupons) + .where("vendorID", isEqualTo: vendorId) + .where("isEnabled", isEqualTo: true) + .where("isPublic", isEqualTo: true) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel favouriteModel = CouponModel.fromJson(element.data()); + couponList.add(favouriteModel); + } + }); + return couponList; + } + + static Future?> getAttributes() async { + List attributeList = []; + await fireStore.collection(CollectionName.vendorAttributes).get().then(( + value, + ) { + for (var element in value.docs) { + AttributesModel favouriteModel = AttributesModel.fromJson( + element.data(), + ); + attributeList.add(favouriteModel); + } + }); + return attributeList; + } + + static Future getVendorCategoryById( + String categoryId, + ) async { + VendorCategoryModel? vendorCategoryModel; + try { + await fireStore + .collection(CollectionName.vendorCategories) + .doc(categoryId) + .get() + .then((value) { + if (value.exists) { + vendorCategoryModel = VendorCategoryModel.fromJson(value.data()!); + } + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return vendorCategoryModel; + } + + static Future> getVendorReviews(String vendorId) async { + List ratingList = []; + await fireStore + .collection(CollectionName.itemsReview) + .where('VendorId', isEqualTo: vendorId) + .get() + .then((value) { + for (var element in value.docs) { + RatingModel giftCardsOrderModel = RatingModel.fromJson( + element.data(), + ); + ratingList.add(giftCardsOrderModel); + } + }); + return ratingList; + } + + static Future getPaymentSettingsData() async { + await fireStore + .collection(CollectionName.settings) + .doc("payFastSettings") + .get() + .then((value) async { + if (value.exists) { + PayFastModel payFastModel = PayFastModel.fromJson(value.data()!); + await Preferences.setString( + Preferences.payFastSettings, + jsonEncode(payFastModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("MercadoPago") + .get() + .then((value) async { + if (value.exists) { + MercadoPagoModel mercadoPagoModel = MercadoPagoModel.fromJson( + value.data()!, + ); + await Preferences.setString( + Preferences.mercadoPago, + jsonEncode(mercadoPagoModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("paypalSettings") + .get() + .then((value) async { + if (value.exists) { + PayPalModel payPalModel = PayPalModel.fromJson(value.data()!); + await Preferences.setString( + Preferences.paypalSettings, + jsonEncode(payPalModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("stripeSettings") + .get() + .then((value) async { + if (value.exists) { + StripeModel stripeModel = StripeModel.fromJson(value.data()!); + await Preferences.setString( + Preferences.stripeSettings, + jsonEncode(stripeModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("flutterWave") + .get() + .then((value) async { + if (value.exists) { + FlutterWaveModel flutterWaveModel = FlutterWaveModel.fromJson( + value.data()!, + ); + await Preferences.setString( + Preferences.flutterWave, + jsonEncode(flutterWaveModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("payStack") + .get() + .then((value) async { + if (value.exists) { + PayStackModel payStackModel = PayStackModel.fromJson(value.data()!); + await Preferences.setString( + Preferences.payStack, + jsonEncode(payStackModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("PaytmSettings") + .get() + .then((value) async { + if (value.exists) { + PaytmModel paytmModel = PaytmModel.fromJson(value.data()!); + await Preferences.setString( + Preferences.paytmSettings, + jsonEncode(paytmModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("walletSettings") + .get() + .then((value) async { + if (value.exists) { + WalletSettingModel walletSettingModel = WalletSettingModel.fromJson( + value.data()!, + ); + await Preferences.setString( + Preferences.walletSettings, + jsonEncode(walletSettingModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("razorpaySettings") + .get() + .then((value) async { + if (value.exists) { + RazorPayModel razorPayModel = RazorPayModel.fromJson(value.data()!); + await Preferences.setString( + Preferences.razorpaySettings, + jsonEncode(razorPayModel.toJson()), + ); + } + }); + await fireStore + .collection(CollectionName.settings) + .doc("CODSettings") + .get() + .then((value) async { + if (value.exists) { + CodSettingModel codSettingModel = CodSettingModel.fromJson( + value.data()!, + ); + await Preferences.setString( + Preferences.codSettings, + jsonEncode(codSettingModel.toJson()), + ); + } + }); + + await fireStore + .collection(CollectionName.settings) + .doc("midtrans_settings") + .get() + .then((value) async { + if (value.exists) { + MidTrans midTrans = MidTrans.fromJson(value.data()!); + await Preferences.setString( + Preferences.midTransSettings, + jsonEncode(midTrans.toJson()), + ); + } + }); + + await fireStore + .collection(CollectionName.settings) + .doc("orange_money_settings") + .get() + .then((value) async { + if (value.exists) { + OrangeMoney orangeMoney = OrangeMoney.fromJson(value.data()!); + await Preferences.setString( + Preferences.orangeMoneySettings, + jsonEncode(orangeMoney.toJson()), + ); + } + }); + + await fireStore + .collection(CollectionName.settings) + .doc("xendit_settings") + .get() + .then((value) async { + if (value.exists) { + Xendit xendit = Xendit.fromJson(value.data()!); + await Preferences.setString( + Preferences.xenditSettings, + jsonEncode(xendit.toJson()), + ); + } + }); + } + + static Future updateUserWallet({ + required String amount, + required String userId, + }) async { + bool isAdded = false; + await getUserProfile(userId).then((value) async { + if (value != null) { + UserModel userModel = value; + print("Old Wallet Amount: ${userModel.walletAmount}"); + print("Amount to Add: $amount"); + userModel.walletAmount = + double.parse(userModel.walletAmount.toString()) + + double.parse(amount); + await FireStoreUtils.updateUser(userModel).then((value) { + isAdded = value; + }); + } + }); + return isAdded; + } + + static StreamController>? + getNearestVendorByCategoryController; + + static Stream> getAllNearestRestaurantByCategoryId({ + bool? isDining, + required String categoryId, + }) async* { + try { + getNearestVendorByCategoryController = + StreamController>.broadcast(); + List vendorList = []; + Query> query = + isDining == true + ? fireStore + .collection(CollectionName.vendors) + .where('categoryID', arrayContains: categoryId) + .where("enabledDiveInFuture", isEqualTo: true) + : fireStore + .collection(CollectionName.vendors) + .where('categoryID', arrayContains: categoryId); + + GeoFirePoint center = Geoflutterfire().point( + latitude: Constant.selectedLocation.location!.latitude ?? 0.0, + longitude: Constant.selectedLocation.location!.longitude ?? 0.0, + ); + String field = 'g'; + + Stream> stream = Geoflutterfire() + .collection(collectionRef: query) + .within( + center: center, + radius: double.parse( + Constant.sectionConstantModel!.nearByRadius.toString(), + ), + field: field, + strictMode: true, + ); + + stream.listen((List documentList) async { + vendorList.clear(); + for (var document in documentList) { + final data = document.data() as Map; + VendorModel vendorModel = VendorModel.fromJson(data); + if ((Constant.isSubscriptionModelApplied == true || + vendorModel.adminCommission?.isEnabled == true) && + vendorModel.subscriptionPlan != null) { + if (vendorModel.subscriptionTotalOrders == "-1") { + vendorList.add(vendorModel); + } else { + if ((vendorModel.subscriptionExpiryDate != null && + vendorModel.subscriptionExpiryDate!.toDate().isBefore( + DateTime.now(), + ) == + false) || + vendorModel.subscriptionPlan?.expiryDay == '-1') { + if (vendorModel.subscriptionTotalOrders != '0') { + vendorList.add(vendorModel); + } + } + } + } else { + vendorList.add(vendorModel); + } + } + getNearestVendorByCategoryController!.sink.add(vendorList); + }); + + yield* getNearestVendorByCategoryController!.stream; + } catch (e) { + print(e); + } + } + + static StreamController>? getNearestVendorController; + + static Stream> getAllNearestRestaurant({ + bool? isDining, + }) async* { + try { + getNearestVendorController = + StreamController>.broadcast(); + List vendorList = []; + Query> query = + isDining == true + ? fireStore + .collection(CollectionName.vendors) + .where( + 'section_id', + isEqualTo: Constant.sectionConstantModel!.id, + ) + .where("enabledDiveInFuture", isEqualTo: true) + : fireStore + .collection(CollectionName.vendors) + .where( + 'section_id', + isEqualTo: Constant.sectionConstantModel!.id, + ); + + GeoFirePoint center = Geoflutterfire().point( + latitude: Constant.selectedLocation.location!.latitude ?? 0.0, + longitude: Constant.selectedLocation.location!.longitude ?? 0.0, + ); + String field = 'g'; + + Stream> stream = Geoflutterfire() + .collection(collectionRef: query) + .within( + center: center, + radius: double.parse( + Constant.sectionConstantModel!.nearByRadius.toString(), + ), + field: field, + strictMode: true, + ); + + stream.listen((List documentList) async { + vendorList.clear(); + for (var document in documentList) { + final data = document.data() as Map; + VendorModel vendorModel = VendorModel.fromJson(data); + if ((Constant.isSubscriptionModelApplied == true || + Constant.sectionConstantModel!.adminCommision?.isEnabled == + true) && + vendorModel.subscriptionPlan != null) { + if (vendorModel.subscriptionTotalOrders == "-1") { + vendorList.add(vendorModel); + } else { + if ((vendorModel.subscriptionExpiryDate != null && + vendorModel.subscriptionExpiryDate!.toDate().isBefore( + DateTime.now(), + ) == + false) || + vendorModel.subscriptionPlan?.expiryDay == "-1") { + if (vendorModel.subscriptionTotalOrders != '0') { + vendorList.add(vendorModel); + } + } + } + } else { + vendorList.add(vendorModel); + } + } + getNearestVendorController!.sink.add(vendorList); + }); + + yield* getNearestVendorController!.stream; + } catch (e) { + print(e); + } + } + + static Future> getHomePageShowCategory() async { + List vendorCategoryList = []; + await fireStore + .collection(CollectionName.vendorCategories) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .where("show_in_homepage", isEqualTo: true) + .where('publish', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + VendorCategoryModel vendorCategoryModel = + VendorCategoryModel.fromJson(element.data()); + vendorCategoryList.add(vendorCategoryModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return vendorCategoryList; + } + + static Future?> getWalletTransaction() async { + List walletTransactionList = []; + log("FireStoreUtils.getCurrentUid() :: ${FireStoreUtils.getCurrentUid()}"); + await fireStore + .collection(CollectionName.wallet) + .where('user_id', isEqualTo: FireStoreUtils.getCurrentUid()) + .orderBy('date', descending: true) + .get() + .then((value) { + for (var element in value.docs) { + WalletTransactionModel walletTransactionModel = + WalletTransactionModel.fromJson(element.data()); + walletTransactionList.add(walletTransactionModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return walletTransactionList; + } + + static Future> getProductListByCategoryId( + String categoryId, + ) async { + List productList = []; + List categorybyProductList = []; + QuerySnapshot> currencyQuery = + await fireStore + .collection(CollectionName.vendorProducts) + .where('categoryID', isEqualTo: categoryId) + .where('publish', isEqualTo: true) + .get(); + await Future.forEach(currencyQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + try { + productList.add(ProductModel.fromJson(document.data())); + } catch (e) { + print('FireStoreUtils.getCurrencys Parse error $e'); + } + }); + + List vendorList = await getAllStoresFuture(); + List allProduct = []; + + for (var vendor in vendorList) { + await getAllProducts(vendor!.id.toString()).then((value) { + if (Constant.isSubscriptionModelApplied == true || + vendor.adminCommission?.isEnabled == true) { + if (vendor.subscriptionPlan != null && + Constant.isExpire(vendor) == false) { + if (vendor.subscriptionPlan?.itemLimit == '-1') { + allProduct.addAll(value); + } else { + int selectedProduct = + value.length < + int.parse(vendor.subscriptionPlan?.itemLimit ?? '0') + ? (value.isEmpty ? 0 : (value.length)) + : int.parse(vendor.subscriptionPlan?.itemLimit ?? '0'); + allProduct.addAll(value.sublist(0, selectedProduct)); + } + } + } else { + allProduct.addAll(value); + } + }); + } + + for (var element in productList) { + bool productIsInList = allProduct.any( + (product) => product.id == element.id, + ); + if (productIsInList) { + categorybyProductList.add(element); + } + } + + return categorybyProductList; + } + + static Future> getAllProducts(String vendorId) async { + List products = []; + + QuerySnapshot> productsQuery = + await fireStore + .collection(CollectionName.vendorProducts) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .where('vendorID', isEqualTo: vendorId) + .where('publish', isEqualTo: true) + .orderBy('createdAt', descending: false) + .get(); + await Future.forEach(productsQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + try { + products.add(ProductModel.fromJson(document.data())); + } catch (e) { + print('product**-FireStoreUtils.getAllProducts Parse error $e'); + } + }); + return products; + } + + static Future> getAllStoresFuture({ + String? categoryId, + }) async { + List vendors = []; + + try { + final collectionReference = + categoryId == null + ? fireStore + .collection(CollectionName.vendors) + .where( + "section_id", + isEqualTo: Constant.sectionConstantModel!.id, + ) + : fireStore + .collection(CollectionName.vendors) + .where( + "section_id", + isEqualTo: Constant.sectionConstantModel!.id, + ) + .where("categoryID", isEqualTo: categoryId); + + GeoFirePoint center = Geoflutterfire().point( + latitude: Constant.selectedLocation.location!.latitude ?? 0.0, + longitude: Constant.selectedLocation.location!.longitude ?? 0.0, + ); + + String field = 'g'; + + List documentList = + await Geoflutterfire() + .collection(collectionRef: collectionReference) + .within( + center: center, + radius: double.parse( + Constant.sectionConstantModel!.nearByRadius.toString(), + ), + field: field, + strictMode: true, + ) + .first; // Fetch the data once as a Future + + if (documentList.isNotEmpty) { + for (var document in documentList) { + final data = document.data() as Map; + VendorModel vendorModel = VendorModel.fromJson(data); + + if (Constant.isSubscriptionModelApplied == true || + Constant.sectionConstantModel?.adminCommision?.isEnabled == + true) { + if (vendorModel.subscriptionPlan != null && + Constant.isExpire(vendorModel) == false) { + if (vendorModel.subscriptionTotalOrders == "-1") { + vendors.add(vendorModel); + } else { + if ((vendorModel.subscriptionExpiryDate != null && + vendorModel.subscriptionExpiryDate!.toDate().isBefore( + DateTime.now(), + ) == + false) || + vendorModel.subscriptionPlan?.expiryDay == "-1") { + if (vendorModel.subscriptionTotalOrders != '0') { + vendors.add(vendorModel); + } + } + } + } + } else { + vendors.add(vendorModel); + } + } + } + } catch (e) { + print('Error fetching vendors: $e'); + } + + return vendors; + } + + static Future getNotificationContent(String type) async { + NotificationModel? notificationModel; + await fireStore + .collection(CollectionName.dynamicNotification) + .where('type', isEqualTo: type) + .get() + .then((value) { + print("------>"); + if (value.docs.isNotEmpty) { + print(value.docs.first.data()); + + notificationModel = NotificationModel.fromJson( + value.docs.first.data(), + ); + } else { + notificationModel = NotificationModel( + id: "", + message: "Notification setup is pending", + subject: "setup notification", + type: "", + ); + } + }); + return notificationModel; + } + + static Future> getVendorCategory() async { + List list = []; + await fireStore + .collection(CollectionName.vendorCategories) + .where('section_id', isEqualTo: Constant.sectionConstantModel!.id) + .where('publish', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + print("====>${value.docs.length}"); + VendorCategoryModel walletTransactionModel = + VendorCategoryModel.fromJson(element.data()); + list.add(walletTransactionModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return list; + } + + static Future placeGiftCardOrder( + GiftCardsOrderModel giftCardsOrderModel, + ) async { + print("=====>"); + print(giftCardsOrderModel.toJson()); + await fireStore + .collection(CollectionName.giftPurchases) + .doc(giftCardsOrderModel.id) + .set(giftCardsOrderModel.toJson()); + return giftCardsOrderModel; + } + + static Future removeFavouriteRestaurant(FavouriteModel favouriteModel) async { + await fireStore + .collection(CollectionName.favoriteVendor) + .where("store_id", isEqualTo: favouriteModel.restaurantId) + .get() + .then((value) { + value.docs.forEach((element) async { + await fireStore + .collection(CollectionName.favoriteVendor) + .doc(element.id) + .delete(); + }); + }); + } + + static Future setFavouriteRestaurant( + FavouriteModel favouriteModel, + ) async { + favouriteModel.sectionId = Constant.sectionConstantModel!.id; + log("setFavouriteRestaurant :: ${favouriteModel.toJson()}"); + await fireStore + .collection(CollectionName.favoriteVendor) + .add(favouriteModel.toJson()); + } + + static Future removeFavouriteItem( + FavouriteItemModel favouriteModel, + ) async { + try { + final favoriteCollection = fireStore.collection( + CollectionName.favoriteItem, + ); + final querySnapshot = + await favoriteCollection + .where("product_id", isEqualTo: favouriteModel.productId) + .get(); + for (final doc in querySnapshot.docs) { + await favoriteCollection.doc(doc.id).delete(); + } + } catch (e) { + print("Error removing favourite item: $e"); + } + } + + static Future setFavouriteItem( + FavouriteItemModel favouriteModel, + ) async { + favouriteModel.sectionId = Constant.sectionConstantModel!.id; + await fireStore + .collection(CollectionName.favoriteItem) + .add(favouriteModel.toJson()); + } + + static Future uploadChatImageToFireStorage( + File image, + BuildContext context, + ) async { + ShowToastDialog.showLoader("Please wait".tr); + var uniqueID = const Uuid().v4(); + Reference upload = FirebaseStorage.instance.ref().child( + 'images/$uniqueID.png', + ); + UploadTask uploadTask = upload.putFile(image); + var storageRef = (await uploadTask.whenComplete(() {})).ref; + var downloadUrl = await storageRef.getDownloadURL(); + var metaData = await storageRef.getMetadata(); + ShowToastDialog.closeLoader(); + return Url( + mime: metaData.contentType ?? 'image', + url: downloadUrl.toString(), + ); + } + + static Future> getHomeCoupon() async { + List list = []; + await fireStore + .collection(CollectionName.coupons) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .where("isEnabled", isEqualTo: true) + .where("isPublic", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel walletTransactionModel = CouponModel.fromJson( + element.data(), + ); + list.add(walletTransactionModel); + } + }) + .catchError((error) { + log(error.toString()); + }); + return list; + } + + static Future> getHomeTopBanner() async { + List bannerList = []; + await fireStore + .collection(CollectionName.bannerItems) + .where("is_publish", isEqualTo: true) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where("position", isEqualTo: "top") + .orderBy("set_order", descending: false) + .get() + .then((value) { + for (var element in value.docs) { + BannerModel bannerHome = BannerModel.fromJson(element.data()); + bannerList.add(bannerHome); + } + }); + return bannerList; + } + + static Future> getStory() async { + List storyList = []; + await fireStore + .collection(CollectionName.story) + .where('sectionID', isEqualTo: Constant.sectionConstantModel!.id) + .get() + .then((value) { + print("Number of Stories Fetched: ${value.docs.length}"); + for (var element in value.docs) { + StoryModel walletTransactionModel = StoryModel.fromJson( + element.data(), + ); + storyList.add(walletTransactionModel); + } + }); + return storyList; + } + + static Future checkRedeemCode(String giftCode) async { + GiftCardsOrderModel? giftCardsOrderModel; + await fireStore + .collection(CollectionName.giftPurchases) + .where("giftCode", isEqualTo: giftCode) + .get() + .then((value) { + if (value.docs.isNotEmpty) { + giftCardsOrderModel = GiftCardsOrderModel.fromJson( + value.docs.first.data(), + ); + } + }); + return giftCardsOrderModel; + } + + static Future sendTopUpMail({ + required String amount, + required String paymentMethod, + required String tractionId, + }) async { + EmailTemplateModel? emailTemplateModel = + await FireStoreUtils.getEmailTemplates(Constant.walletTopup); + + String newString = emailTemplateModel!.message.toString(); + newString = newString.replaceAll( + "{username}", + Constant.userModel!.firstName.toString() + + Constant.userModel!.lastName.toString(), + ); + newString = newString.replaceAll( + "{date}", + DateFormat('yyyy-MM-dd').format(Timestamp.now().toDate()), + ); + newString = newString.replaceAll( + "{amount}", + Constant.amountShow(amount: amount), + ); + newString = newString.replaceAll( + "{paymentmethod}", + paymentMethod.toString(), + ); + newString = newString.replaceAll("{transactionid}", tractionId.toString()); + newString = newString.replaceAll( + "{newwalletbalance}.", + Constant.amountShow(amount: Constant.userModel!.walletAmount.toString()), + ); + await Constant.sendMail( + subject: emailTemplateModel.subject, + isAdmin: emailTemplateModel.isSendToAdmin, + body: newString, + recipients: [Constant.userModel!.email], + ); + } + + static Future uploadChatVideoToFireStorage( + BuildContext context, + File video, + ) async { + try { + ShowToastDialog.showLoader("Uploading video..."); + final String uniqueID = const Uuid().v4(); + final Reference videoRef = FirebaseStorage.instance.ref( + 'videos/$uniqueID.mp4', + ); + final UploadTask uploadTask = videoRef.putFile( + video, + SettableMetadata(contentType: 'video/mp4'), + ); + await uploadTask; + final String videoUrl = await videoRef.getDownloadURL(); + ShowToastDialog.showLoader("Generating thumbnail..."); + File thumbnail = await VideoCompress.getFileThumbnail( + video.path, + quality: 75, // 0 - 100 + position: -1, // Get the first frame + ); + + final String thumbnailID = const Uuid().v4(); + final Reference thumbnailRef = FirebaseStorage.instance.ref( + 'thumbnails/$thumbnailID.jpg', + ); + final UploadTask thumbnailUploadTask = thumbnailRef.putData( + thumbnail.readAsBytesSync(), + SettableMetadata(contentType: 'image/jpeg'), + ); + await thumbnailUploadTask; + final String thumbnailUrl = await thumbnailRef.getDownloadURL(); + var metaData = await thumbnailRef.getMetadata(); + ShowToastDialog.closeLoader(); + + return ChatVideoContainer( + videoUrl: Url( + url: videoUrl.toString(), + mime: metaData.contentType ?? 'video', + videoThumbnail: thumbnailUrl, + ), + thumbnailUrl: thumbnailUrl, + ); + } catch (e) { + ShowToastDialog.closeLoader(); + ShowToastDialog.showToast("Error: ${e.toString()}"); + return null; + } + } + + static Future getOrderByOrderId(String orderId) async { + OrderModel? orderModel; + try { + await fireStore + .collection(CollectionName.vendorOrders) + .doc(orderId) + .get() + .then((value) { + if (value.data() != null) { + orderModel = OrderModel.fromJson(value.data()!); + } + }); + } catch (e, s) { + print('FireStoreUtils.firebaseCreateNewUser $e $s'); + return null; + } + return orderModel; + } + + static Future> getCabCoupon() async { + List ordersList = []; + await fireStore + .collection(CollectionName.promos) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .where("isEnabled", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel bannerHome = CouponModel.fromJson(element.data()); + ordersList.add(bannerHome); + } + }); + return ordersList; + } + + static Future> getParcelCoupon() async { + List ordersList = []; + await fireStore + .collection(CollectionName.parcelCoupons) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .where("isEnabled", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel bannerHome = CouponModel.fromJson(element.data()); + ordersList.add(bannerHome); + } + }); + return ordersList; + } + + static Future> getRentalCoupon() async { + List ordersList = []; + await fireStore + .collection(CollectionName.rentalCoupons) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .where("isEnabled", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel bannerHome = CouponModel.fromJson(element.data()); + ordersList.add(bannerHome); + } + }); + return ordersList; + } + + static Future deleteUser() async { + bool? isDelete; + try { + await fireStore + .collection(CollectionName.users) + .doc(FireStoreUtils.getCurrentUid()) + .delete(); + + // delete user from firebase auth + await auth.FirebaseAuth.instance.currentUser?.delete().then((value) { + isDelete = true; + }); + } catch (e, s) { + log('FireStoreUtils.firebaseCreateNewUser $e $s'); + return false; + } + return isDelete; + } + + static Future> getParcelServiceCategory() async { + List parcelCategoryList = []; + await fireStore + .collection(CollectionName.parcelCategory) + .where('publish', isEqualTo: true) + // .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .orderBy('set_order', descending: false) + .get() + .then((value) { + log("Parcel Category Count: ${value.docs.length}"); + for (var element in value.docs) { + try { + ParcelCategory category = ParcelCategory.fromJson(element.data()); + parcelCategoryList.add(category); + } catch (e, stackTrace) { + log('getParcelServiceCategory parse error: ${element.id} $e'); + log(stackTrace.toString()); + } + } + }); + return parcelCategoryList; + } + + static Future> getParcelWeight() async { + List parcelWeightList = []; + await fireStore.collection(CollectionName.parcelWeight).get().then((value) { + for (var element in value.docs) { + try { + ParcelWeightModel category = ParcelWeightModel.fromJson( + element.data(), + ); + parcelWeightList.add(category); + } catch (e, stackTrace) { + log('getParcelWeight parse error: ${element.id} $e'); + log(stackTrace.toString()); + } + } + }); + return parcelWeightList; + } + + static Future setParcelOrder( + ParcelOrderModel orderModel, + double totalAmount, + ) async { + // try { + // final firestore = FirebaseFirestore.instance; + // final isNew = orderModel.id.isEmpty; + // + // final docRef = firestore.collection(CollectionName.parcelOrders).doc(isNew ? null : orderModel.id); + // if (isNew) { + // orderModel.id = docRef.id; + // } + // + // // Handle wallet payment if needed + // if (orderModel.paymentCollectByReceiver == false && orderModel.paymentMethod == "wallet") { + // WalletTransactionModel transactionModel = WalletTransactionModel( + // id: Constant.getUuid(), + // serviceType: 'parcel-service', + // amount: totalAmount, + // date: Timestamp.now(), + // paymentMethod: PaymentGateway.wallet.name, + // transactionUser: "customer", + // userId: FireStoreUtils.getCurrentUid(), + // isTopup: false, + // orderId: orderModel.id, + // note: "Order Amount debited".tr, + // paymentStatus: "success".tr, + // ); + // + // await FireStoreUtils.setWalletTransaction(transactionModel).then((value) async { + // if (value == true) { + // await FireStoreUtils.updateUserWallet(amount: "-$totalAmount", userId: FireStoreUtils.getCurrentUid()); + // } + // }); + // } + // + // // Set the parcel order in Firestore + // await firestore.collection(CollectionName.parcelOrders).doc(orderModel.id).set(orderModel.toJson()); + // + // return true; + // } catch (e) { + // debugPrint("Failed to place parcel order: $e"); + // return false; + // } + return true; + } + + static Future sendParcelBookEmail({ + required ParcelOrderModel orderModel, + }) async { + try { + EmailTemplateModel? emailTemplateModel = + await FireStoreUtils.getEmailTemplates(Constant.newParcelBook); + + String newString = emailTemplateModel!.message.toString(); + newString = newString.replaceAll( + "{passengername}", + "${Constant.userModel!.firstName} ${Constant.userModel!.lastName}", + ); + newString = newString.replaceAll("{parcelid}", orderModel.id.toString()); + newString = newString.replaceAll( + "{date}", + DateFormat('dd-MM-yyyy').format(orderModel.createdAt!.toDate()), + ); + newString = newString.replaceAll( + "{sendername}", + orderModel.sender!.name.toString(), + ); + newString = newString.replaceAll( + "{senderphone}", + orderModel.sender!.phone.toString(), + ); + newString = newString.replaceAll("{note}", orderModel.note.toString()); + newString = newString.replaceAll( + "{deliverydate}", + DateFormat( + 'dd-MM-yyyy', + ).format(orderModel.receiverPickupDateTime!.toDate()), + ); + + String subjectNewString = emailTemplateModel.subject.toString(); + subjectNewString = subjectNewString.replaceAll( + "{orderid}", + orderModel.id.toString(), + ); + await Constant.sendMail( + subject: subjectNewString, + isAdmin: emailTemplateModel.isSendToAdmin, + body: newString, + recipients: [Constant.userModel!.email], + ); + } catch (e) { + log("SIGNUP :: 22 :::::: $e"); + } + } + + static Stream> listenParcelOrders() { + return fireStore + .collection(CollectionName.parcelOrders) + .where('authorID', isEqualTo: FireStoreUtils.getCurrentUid()) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .orderBy('createdAt', descending: true) + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) { + log("===>"); + print(doc.data()); + return ParcelOrderModel.fromJson(doc.data()); + }).toList(); + }); + } + + static Future> getVehicleType() async { + List vehicleTypeList = []; + await fireStore + .collection(CollectionName.vehicleType) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .where("isActive", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + try { + VehicleType category = VehicleType.fromJson(element.data()); + vehicleTypeList.add(category); + } catch (e, stackTrace) { + print('getVehicleType error: ${element.id} $e'); + print(stackTrace); + } + } + }); + return vehicleTypeList; + } + + static Future> getPopularDestination() async { + List popularDestination = []; + await fireStore + .collection(CollectionName.popularDestinations) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where('is_publish', isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + try { + PopularDestination category = PopularDestination.fromJson( + element.data(), + ); + popularDestination.add(category); + } catch (e, stackTrace) { + print('Get PopularDestination error: ${element.id} $e'); + print(stackTrace); + } + } + }); + return popularDestination; + } + + static Future cabOrderPlace(CabOrderModel orderModel) async { + await fireStore + .collection(CollectionName.rides) + .doc(orderModel.id) + .set(orderModel.toJson()); + } + + static Future parcelOrderPlace(ParcelOrderModel orderModel) async { + await fireStore + .collection(CollectionName.parcelOrders) + .doc(orderModel.id) + .set(orderModel.toJson()); + } + + static Future rentalOrderPlace(RentalOrderModel orderModel) async { + await fireStore + .collection(CollectionName.rentalOrders) + .doc(orderModel.id) + .set(orderModel.toJson()); + } + + static Future getCabOrderById(String orderId) async { + CabOrderModel? orderModel; + try { + final doc = + await fireStore.collection(CollectionName.rides).doc(orderId).get(); + if (doc.data() != null) { + final model = CabOrderModel.fromJson(doc.data()!); + if (model.rideType == "ride") { + orderModel = model; + } + } + } catch (e, s) { + print('getCabOrderById error: $e\n$s'); + return null; + } + return orderModel; + } + + static Future getIntercityOrder(String orderId) async { + CabOrderModel? orderModel; + try { + final doc = + await fireStore.collection(CollectionName.rides).doc(orderId).get(); + if (doc.data() != null) { + final model = CabOrderModel.fromJson(doc.data()!); + if (model.rideType == "intercity") { + orderModel = model; + } + } + } catch (e, s) { + print('getCabOrderById error: $e\n$s'); + return null; + } + return orderModel; + } + + static Future getDriver(String userId) async { + UserModel? userModel; + + try { + final doc = + await fireStore.collection(CollectionName.users).doc(userId).get(); + + if (doc.data() != null) { + userModel = UserModel.fromJson(doc.data()!); + } + } catch (e) { + log("getDriver error: $e"); + } + + return userModel; + } + + // static Future> getCabDriverOrders() async { + // List ordersList = []; + // await fireStore.collection(CollectionName.rides).where('authorID', isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true).get().then((value) { + // for (var element in value.docs) { + // CabOrderModel orderModel = CabOrderModel.fromJson(element.data()); + // ordersList.add(orderModel); + // } + // }); + // return ordersList; + // } + + static Stream> getCabDriverOrders() { + return fireStore + .collection(CollectionName.rides) + .where('authorID', isEqualTo: FireStoreUtils.getCurrentUid()) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .orderBy('createdAt', descending: true) + .snapshots() + .map((query) { + List ordersList = []; + for (var element in query.docs) { + ordersList.add(CabOrderModel.fromJson(element.data())); + } + return ordersList; + }); + } + + static Future> getOnDemandCategory() async { + List categoryList = []; + await fireStore + .collection(CollectionName.providerCategories) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where("level", isEqualTo: 0) + .where("publish", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + CategoryModel orderModel = CategoryModel.fromJson(element.data()); + categoryList.add(orderModel); + } + }); + return categoryList; + } + + static Future getCategoryById(String categoryId) async { + CategoryModel? categoryModel; + await fireStore + .collection(CollectionName.providerCategories) + .doc(categoryId) + .get() + .then((value) { + if (value.exists) { + categoryModel = CategoryModel.fromJson(value.data()!); + } + }); + return categoryModel; + } + + static Future> getProviderFuture({ + String categoryId = '', + }) async { + List providerList = []; + + try { + Query> collectionReference; + + if (categoryId.isNotEmpty) { + collectionReference = fireStore + .collection(CollectionName.providersServices) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where('categoryId', isEqualTo: categoryId) + .where("publish", isEqualTo: true); + } else { + collectionReference = fireStore + .collection(CollectionName.providersServices) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where("publish", isEqualTo: true); + } + + GeoFirePoint center = Geoflutterfire().point( + latitude: Constant.selectedLocation.location!.latitude ?? 0.0, + longitude: Constant.selectedLocation.location!.longitude ?? 0.0, + ); + + String field = 'g'; + + await Geoflutterfire() + .collection(collectionRef: collectionReference) + .within( + center: center, + radius: double.parse( + Constant.sectionConstantModel!.nearByRadius.toString(), + ), + field: field, + strictMode: true, + ) + .first + .then((documentList) { + for (var document in documentList) { + ProviderServiceModel providerServiceModel = + ProviderServiceModel.fromJson( + document.data() as Map, + ); + + log( + ":: isExpireDate(expiryDay :: ${Constant.isExpireDate(expiryDay: (providerServiceModel.subscriptionPlan?.expiryDay == '-1'), subscriptionExpiryDate: providerServiceModel.subscriptionExpiryDate)}", + ); + + if (Constant.isSubscriptionModelApplied == true || + Constant.sectionConstantModel?.adminCommision?.isEnabled == + true) { + if (providerServiceModel.subscriptionPlan != null && + Constant.isExpireDate( + expiryDay: + (providerServiceModel + .subscriptionPlan + ?.expiryDay == + '-1'), + subscriptionExpiryDate: + providerServiceModel.subscriptionExpiryDate, + ) == + false) { + if (providerServiceModel.subscriptionTotalOrders == "-1" || + providerServiceModel.subscriptionTotalOrders != '0') { + providerList.add(providerServiceModel); + } + } + } else { + providerList.add(providerServiceModel); + } + } + }) + .catchError((error) { + log('Error fetching providers: $error'); + }); + } catch (e) { + log('Error in getProviderFuture: $e'); + } + + return providerList; + } + + static Future> getAllProviderServiceByAuthorId( + String authId, + ) async { + List providerService = []; + await fireStore + .collection(CollectionName.providersServices) + .where('author', isEqualTo: authId) + .where('publish', isEqualTo: true) + .orderBy('createdAt', descending: false) + .get() + .then((value) { + for (var element in value.docs) { + ProviderServiceModel orderModel = ProviderServiceModel.fromJson( + element.data(), + ); + providerService.add(orderModel); + } + }); + return providerService; + } + + static Future getSubCategoryById(String categoryId) async { + CategoryModel? categoryModel; + await fireStore + .collection(CollectionName.providerCategories) + .doc(categoryId) + .get() + .then((value) { + if (value.exists) { + categoryModel = CategoryModel.fromJson(value.data()!); + } + }); + return categoryModel; + } + + static Future> getReviewByProviderServiceId( + String serviceId, + ) async { + List providerReview = []; + await fireStore + .collection(CollectionName.itemsReview) + .where('productId', isEqualTo: serviceId) + .get() + .then((value) { + for (var element in value.docs) { + RatingModel orderModel = RatingModel.fromJson(element.data()); + providerReview.add(orderModel); + } + }); + return providerReview; + } + + static Future> getProviderServiceByProviderId({ + required String providerId, + }) async { + List providerList = []; + + try { + final collectionReference = fireStore + .collection(CollectionName.providersServices) + .where("author", isEqualTo: providerId) + .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id) + .where("publish", isEqualTo: true); + + // Geolocation center point + GeoFirePoint center = Geoflutterfire().point( + latitude: Constant.selectedLocation.location!.latitude ?? 0.0, + longitude: Constant.selectedLocation.location!.longitude ?? 0.0, + ); + + String field = 'g'; + + // Query within radius + await Geoflutterfire() + .collection(collectionRef: collectionReference) + .within( + center: center, + radius: double.parse( + Constant.sectionConstantModel!.nearByRadius.toString(), + ), + field: field, + strictMode: true, + ) + .first + .then((documentList) { + for (var document in documentList) { + ProviderServiceModel providerServiceModel = + ProviderServiceModel.fromJson( + document.data() as Map, + ); + + log( + ":: isExpireDate(expiryDay :: ${Constant.isExpireDate(expiryDay: (providerServiceModel.subscriptionPlan?.expiryDay == '-1'), subscriptionExpiryDate: providerServiceModel.subscriptionExpiryDate)}", + ); + + //Subscription & Commission check + if (Constant.isSubscriptionModelApplied == true || + Constant.sectionConstantModel?.adminCommision?.isEnabled == + true) { + if (providerServiceModel.subscriptionPlan != null && + Constant.isExpireDate( + expiryDay: + (providerServiceModel + .subscriptionPlan + ?.expiryDay == + '-1'), + subscriptionExpiryDate: + providerServiceModel.subscriptionExpiryDate, + ) == + false) { + if (providerServiceModel.subscriptionTotalOrders == "-1" || + providerServiceModel.subscriptionTotalOrders != '0') { + providerList.add(providerServiceModel); + } + } + } else { + providerList.add(providerServiceModel); + } + } + }) + .catchError((error) { + log('Error fetching provider services: $error'); + }); + } catch (e) { + log('Error in getProviderServiceByProviderId: $e'); + } + + return providerList; + } + + static Future> getProviderCoupon(String providerId) async { + List offers = []; + await fireStore + .collection(CollectionName.providersCoupons) + .where('providerId', isEqualTo: providerId) + .where("isEnabled", isEqualTo: true) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel favouriteOndemandServiceModel = CouponModel.fromJson( + element.data(), + ); + offers.add(favouriteOndemandServiceModel); + } + }); + return offers; + } + + static Future> getProviderCouponAfterExpire( + String providerId, + ) async { + List coupon = []; + await fireStore + .collection(CollectionName.providersCoupons) + .where('providerId', isEqualTo: providerId) + .where('isEnabled', isEqualTo: true) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .where('isPublic', isEqualTo: true) + .where('expiresAt', isGreaterThanOrEqualTo: Timestamp.now()) + .get() + .then((value) { + for (var element in value.docs) { + CouponModel favouriteOndemandServiceModel = CouponModel.fromJson( + element.data(), + ); + coupon.add(favouriteOndemandServiceModel); + } + }); + return coupon; + } + + static Future onDemandOrderPlace( + OnProviderOrderModel orderModel, + double totalAmount, + ) async { + DocumentReference documentReference; + if (orderModel.id.isEmpty) { + documentReference = + fireStore.collection(CollectionName.providerOrders).doc(); + orderModel.id = documentReference.id; + } else { + documentReference = fireStore + .collection(CollectionName.providerOrders) + .doc(orderModel.id); + } + await documentReference.set(orderModel.toJson()); + + return orderModel; + } + + static Future sendOrderOnDemandServiceEmail({ + required OnProviderOrderModel orderModel, + }) async { + try { + String firstHTML = """ + + + + + + + + + + + """; + + EmailTemplateModel? emailTemplateModel = + await FireStoreUtils.getEmailTemplates(Constant.newOnDemandBook); + + if (emailTemplateModel != null) { + String newString = emailTemplateModel.message.toString(); + newString = newString.replaceAll( + "{username}", + "${Constant.userModel?.firstName ?? ''} ${Constant.userModel?.lastName ?? ''}", + ); + newString = newString.replaceAll("{orderid}", orderModel.id); + newString = newString.replaceAll( + "{date}", + DateFormat('dd-MM-yyyy').format(orderModel.createdAt.toDate()), + ); + newString = newString.replaceAll( + "{address}", + orderModel.address!.getFullAddress(), + ); + newString = newString.replaceAll( + "{paymentmethod}", + orderModel.payment_method, + ); + + double total = 0.0; + double discount = 0.0; + double taxAmount = 0.0; + List htmlList = []; + + if (orderModel.provider.disPrice == "" || + orderModel.provider.disPrice == "0") { + total = + double.parse(orderModel.provider.price.toString()) * + orderModel.quantity; + } else { + total = + double.parse(orderModel.provider.disPrice.toString()) * + orderModel.quantity; + } + + String product = """ + + + + + + + """; + htmlList.add(product); + + if (orderModel.couponCode != null && + orderModel.couponCode!.isNotEmpty) { + discount = double.parse(orderModel.discount.toString()); + } + List taxHtmlList = []; + if (orderModel.taxModel != null) { + for (var element in orderModel.taxModel!) { + taxAmount = + taxAmount + + Constant.getTaxValue( + amount: (total - discount).toString(), + taxModel: element, + ); + String taxHtml = + """${element.title}: ${Constant.amountShow(amount: Constant.getTaxValue(amount: (total - discount).toString(), taxModel: element).toString())}${Constant.taxList.indexOf(element) == Constant.taxList.length - 1 ? "" : "
"}"""; + taxHtmlList.add(taxHtml); + } + } + + var totalamount = total + taxAmount - discount; + + newString = newString.replaceAll( + "{subtotal}", + Constant.amountShow(amount: total.toString()), + ); + newString = newString.replaceAll( + "{coupon}", + '(${orderModel.couponCode.toString()})', + ); + newString = newString.replaceAll( + "{discountamount}", + orderModel.couponCode == null + ? "0.0" + : Constant.amountShow(amount: orderModel.discount.toString()), + ); + newString = newString.replaceAll( + "{totalAmount}", + Constant.amountShow(amount: totalamount.toString()), + ); + + String tableHTML = htmlList.join(); + String lastHTML = "
Product Name
Quantity
Price
Total
${orderModel.provider.title}${orderModel.quantity}${Constant.amountShow(amount: (orderModel.provider.disPrice == "" || orderModel.provider.disPrice == "0") ? orderModel.provider.price.toString() : orderModel.provider.disPrice.toString())}${Constant.amountShow(amount: (total).toString())}
"; + newString = newString.replaceAll( + "{productdetails}", + firstHTML + tableHTML + lastHTML, + ); + newString = newString.replaceAll("{taxdetails}", taxHtmlList.join()); + newString = newString.replaceAll( + "{newwalletbalance}.", + Constant.amountShow( + amount: Constant.userModel?.walletAmount.toString(), + ), + ); + + String subjectNewString = emailTemplateModel.subject.toString(); + subjectNewString = subjectNewString.replaceAll( + "{orderid}", + orderModel.id, + ); + await Constant.sendMail( + subject: subjectNewString, + isAdmin: emailTemplateModel.isSendToAdmin, + body: newString, + recipients: [Constant.userModel?.email], + ); + } + } catch (e) { + log("SIGNUP :: 22 :::::: $e"); + } + } + + static Future updateOnDemandOrder( + OnProviderOrderModel orderModel, + ) async { + if (orderModel.id.isEmpty) { + throw Exception("Order ID cannot be empty"); + } + + try { + final docRef = fireStore + .collection(CollectionName.providerOrders) + .doc(orderModel.id); + await docRef.set(orderModel.toJson(), SetOptions(merge: true)); + } catch (e) { + print("Error updating OnDemand order: $e"); + rethrow; + } + } + + // static Future updateOnDemandOrder(OnProviderOrderModel orderModel) async { + // if (orderModel.id.isEmpty) { + // throw Exception("Order ID cannot be empty"); + // } + // + // try { + // final docRef = fireStore.collection(CollectionName.providerOrders).doc(orderModel.id); + // + // // Convert model to map + // final Map data = orderModel.toJson(); + // + // // Remove null values so we only update non-null fields + // final Map updateData = {}; + // data.forEach((key, value) { + // if (value != null) { + // updateData[key] = value; + // } + // }); + // + // if (updateData.isNotEmpty) { + // await docRef.set(updateData, SetOptions(merge: true)); + // print("Order ${orderModel.id} updated dynamically: $updateData"); + // } else { + // print("No fields to update for order ${orderModel.id}"); + // } + // } catch (e) { + // print("Error updating OnDemand order: $e"); + // rethrow; + // } + // } + + // static Future> getProviderOrders() async { + // List ordersList = []; + // await fireStore + // .collection(CollectionName.providerOrders) + // .where("authorID", isEqualTo: FireStoreUtils.getCurrentUid()) + // .where("sectionId", isEqualTo: Constant.sectionConstantModel!.id.toString()) + // .orderBy("createdAt", descending: true) + // .get() + // .then((value) { + // for (var element in value.docs) { + // OnProviderOrderModel orderModel = OnProviderOrderModel.fromJson(element.data()); + // ordersList.add(orderModel); + // } + // }); + // return ordersList; + // } + + static Stream> getProviderOrdersStream() { + return fireStore + .collection(CollectionName.providerOrders) + .where("authorID", isEqualTo: getCurrentUid()) + .where( + "sectionId", + isEqualTo: Constant.sectionConstantModel!.id.toString(), + ) + .orderBy("createdAt", descending: true) + .snapshots() + .map( + (snapshot) => + snapshot.docs + .map((doc) => OnProviderOrderModel.fromJson(doc.data())) + .toList(), + ); + } + + static Future getWorker(String id) async { + try { + DocumentSnapshot> doc = + await fireStore + .collection(CollectionName.providersWorkers) + .doc(id) + .get(); + + if (doc.exists && doc.data() != null) { + return WorkerModel.fromJson(doc.data()!); + } + } catch (e) { + print("FireStoreUtils.getWorker error: $e"); + } + return null; + } + + static Future getProviderOrderById( + String orderId, + ) async { + OnProviderOrderModel? orderModel; + await fireStore + .collection(CollectionName.providerOrders) + .doc(orderId) + .get() + .then((value) { + if (value.exists) { + orderModel = OnProviderOrderModel.fromJson(value.data()!); + } + }); + return orderModel; + } + + static Future getReviewsByProviderID( + String orderId, + String providerId, + ) async { + RatingModel? ratingModel; + + await fireStore + .collection(CollectionName.itemsReview) + .where('orderid', isEqualTo: orderId) + .where('VendorId', isEqualTo: providerId) + .limit(1) + .get() + .then((snapshot) { + if (snapshot.docs.isNotEmpty) { + ratingModel = RatingModel.fromJson(snapshot.docs.first.data()); + } + }) + .catchError((error) { + print('Error fetching review for provider: $error'); + }); + + return ratingModel; + } + + static Future getReviewsByWorkerID( + String orderId, + String workerId, + ) async { + RatingModel? ratingModel; + + await fireStore + .collection(CollectionName.itemsReview) + .where('orderid', isEqualTo: orderId) + .where('driverId', isEqualTo: workerId) + .limit(1) + .get() + .then((snapshot) { + if (snapshot.docs.isNotEmpty) { + ratingModel = RatingModel.fromJson(snapshot.docs.first.data()); + } + }) + .catchError((error) { + print('Error fetching review by worker ID: $error'); + }); + + return ratingModel; + } + + static Future getCurrentProvider(String uid) async { + try { + final doc = + await fireStore + .collection(CollectionName.providersServices) + .doc(uid) + .get(); + if (doc.exists && doc.data() != null) { + return ProviderServiceModel.fromJson(doc.data()!); + } + } catch (e, stackTrace) { + print('Error fetching current provider: $e'); + print(stackTrace); + } + return null; + } + + static Future updateReviewById( + RatingModel ratingProduct, + ) async { + try { + await fireStore + .collection(CollectionName.itemsReview) + .doc(ratingProduct.id) + .set(ratingProduct.toJson()); + return ratingProduct; + } catch (e, stackTrace) { + print('Error updating review: $e'); + print(stackTrace); + return null; + } + } + + static Future updateProvider( + ProviderServiceModel provider, + ) async { + try { + await fireStore + .collection(CollectionName.providersServices) + .doc(provider.id) + .set(provider.toJson()); + return provider; + } catch (e, stackTrace) { + print('Error updating provider: $e'); + print(stackTrace); + return null; + } + } + + static Future updateWorker(WorkerModel worker) async { + try { + await fireStore + .collection(CollectionName.providersWorkers) + .doc(worker.id) + .set(worker.toJson()); + return worker; + } catch (e, stackTrace) { + print('Error updating worker: $e'); + print(stackTrace); + return null; + } + } + + static Future getParcelOrder(String orderId) async { + try { + final doc = + await fireStore + .collection(CollectionName.parcelOrders) + .doc(orderId) + .get(); + if (doc.exists && doc.data() != null) { + return ParcelOrderModel.fromJson(doc.data()!); + } + } catch (e, stackTrace) { + print('Error fetching current provider: $e'); + print(stackTrace); + } + return null; + } + + static Stream driverStream(String userId) { + return fireStore + .collection(CollectionName.users) + .doc(userId) + .snapshots() + .map((doc) { + if (doc.data() != null) { + return UserModel.fromJson(doc.data()!); + } + return null; + }); + } + + static Future updateCabOrder(CabOrderModel orderModel) async { + if (orderModel.id!.isEmpty) { + throw Exception("Order ID cannot be empty"); + } + + try { + final docRef = fireStore + .collection(CollectionName.rides) + .doc(orderModel.id); + await docRef.set(orderModel.toJson(), SetOptions(merge: true)); + } catch (e) { + print("Error updating OnDemand order: $e"); + rethrow; + } + } + + static Future> getRentalVehicleType() async { + List vehicleTypeList = []; + await fireStore + .collection(CollectionName.rentalVehicleType) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .where("isActive", isEqualTo: true) + .get() + .then((value) { + for (var element in value.docs) { + try { + RentalVehicleType category = RentalVehicleType.fromJson( + element.data(), + ); + vehicleTypeList.add(category); + } catch (e, stackTrace) { + print('getVehicleType error: ${element.id} $e'); + print(stackTrace); + } + } + }); + return vehicleTypeList; + } + + static Future> getRentalPackage( + String vehicleId, + ) async { + List rentalPackageList = []; + await fireStore + .collection(CollectionName.rentalPackages) + .where("vehicleTypeId", isEqualTo: vehicleId) + .orderBy("ordering", descending: false) + .get() + .then((value) { + for (var element in value.docs) { + try { + log('Rental Package Data: ${element.data()}'); + RentalPackageModel category = RentalPackageModel.fromJson( + element.data(), + ); + rentalPackageList.add(category); + } catch (e, stackTrace) { + print('getVehicleType error: ${element.id} $e'); + print(stackTrace); + } + } + }); + return rentalPackageList; + } + + static Stream> getRentalOrders() { + return fireStore + .collection(CollectionName.rentalOrders) + .where('authorID', isEqualTo: FireStoreUtils.getCurrentUid()) + .where('sectionId', isEqualTo: Constant.sectionConstantModel!.id) + .orderBy('createdAt', descending: true) + .snapshots() + .map((query) { + List ordersList = []; + for (var element in query.docs) { + ordersList.add(RentalOrderModel.fromJson(element.data())); + } + return ordersList; + }); + } + + static Future getRentalOrderById(String orderId) async { + RentalOrderModel? orderModel; + await fireStore + .collection(CollectionName.rentalOrders) + .doc(orderId) + .get() + .then((value) { + if (value.exists) { + orderModel = RentalOrderModel.fromJson(value.data()!); + } + }); + return orderModel; + } + + static Future getReviewsbyID(String orderId) async { + RatingModel? ratingModel; + + await fireStore + .collection(CollectionName.itemsReview) + .where('orderid', isEqualTo: orderId) + .get() + .then((snapshot) { + if (snapshot.docs.isNotEmpty) { + ratingModel = RatingModel.fromJson(snapshot.docs.first.data()); + } + }) + .catchError((error) { + print('Error fetching review for provider: $error'); + }); + + return ratingModel; + } + + static Future getOrderByIdFromAllCollections(String orderId) async { + final List collections = [ + CollectionName.parcelOrders, + CollectionName.rentalOrders, + CollectionName.providerOrders, + CollectionName.rides, + CollectionName.vendorOrders, + ]; + + for (String collection in collections) { + try { + final snapshot = + await fireStore + .collection(collection) + .where('id', isEqualTo: orderId) + .limit(1) + .get(); + + if (snapshot.docs.isNotEmpty) { + final data = snapshot.docs.first.data(); + data['collection_name'] = collection; + return data; + } + } catch (e) { + log("Error fetching from $collection => $e"); + } + } + + log("No order found with ID $orderId"); + return null; + } + + static Future setSos(String orderId, UserLocation userLocation) { + DocumentReference documentReference = + fireStore.collection(CollectionName.sos).doc(); + + Map sosMap = { + 'id': documentReference.id, + 'orderId': orderId, + 'status': "Initiated", + 'latLong': userLocation.toJson(), + }; + + return documentReference + .set(sosMap) + .then((_) { + print("SOS request created successfully for order: $orderId"); + }) + .catchError((error) { + print("Failed to create SOS request: $error"); + }); + } + + static Future getSOS(String orderId) { + return fireStore + .collection(CollectionName.sos) + .where('orderId', isEqualTo: orderId) + .get() + .then((querySnapshot) { + bool isAdded = false; + for (var element in querySnapshot.docs) { + if (element['orderId'] == orderId) { + isAdded = true; + break; + } + } + return isAdded; + }) + .catchError((error) { + print("Error checking SOS: $error"); + return false; + }); + } + + static Future setRideComplain({ + required String orderId, + required String title, + required String description, + required String driverID, + required String driverName, + required String customerID, + required String customerName, + }) async { + try { + DocumentReference docRef = + fireStore.collection(CollectionName.complaints).doc(); + + Map complaintData = { + 'id': docRef.id, + 'createdAt': Timestamp.now(), + 'description': description, + 'driverId': driverID, + 'driverName': driverName, + 'orderId': orderId, + 'customerName': customerName, + 'customerId': customerID, + 'status': "Initiated", + 'title': title, + }; + + await docRef.set(complaintData); + } catch (e) { + print("Error adding ride complain: $e"); + rethrow; + } + } + + static Future isRideComplainAdded(String orderId) async { + try { + QuerySnapshot querySnapshot = + await fireStore + .collection(CollectionName.complaints) + .where('orderId', isEqualTo: orderId) + .limit(1) + .get(); + + return querySnapshot.docs.isNotEmpty; + } catch (e) { + print("Error checking ride complain: $e"); + return false; + } + } + + static Future?> getRideComplainData( + String orderId, + ) async { + try { + QuerySnapshot querySnapshot = + await fireStore + .collection(CollectionName.complaints) + .where('orderId', isEqualTo: orderId) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + return querySnapshot.docs.first.data() as Map; + } else { + return null; + } + } catch (e) { + print("Error fetching ride complain data: $e"); + return null; + } + } + + static void removeFavouriteOndemandService( + FavouriteOndemandServiceModel favouriteModel, + ) { + FirebaseFirestore.instance + .collection(CollectionName.favoriteService) + .where("user_id", isEqualTo: favouriteModel.user_id) + .where("service_id", isEqualTo: favouriteModel.service_id) + .get() + .then((value) { + for (var element in value.docs) { + FirebaseFirestore.instance + .collection(CollectionName.favoriteService) + .doc(element.id) + .delete() + .then((value) { + print("Remove Success!"); + }); + } + }); + } + + static Future setFavouriteOndemandSection( + FavouriteOndemandServiceModel favouriteModel, + ) async { + await fireStore + .collection(CollectionName.favoriteService) + .add(favouriteModel.toJson()) + .then((value) { + print("===FAVOURITE ADDED=== ${favouriteModel.toJson()}"); + }); + } + + static Future> getFavouritesServiceList( + String userId, + ) async { + List lstFavourites = []; + + QuerySnapshot> favourites = + await fireStore + .collection(CollectionName.favoriteService) + .where('user_id', isEqualTo: userId) + .where("section_id", isEqualTo: Constant.sectionConstantModel!.id) + .get(); + + await Future.forEach(favourites.docs, ( + QueryDocumentSnapshot> document, + ) { + try { + lstFavourites.add( + FavouriteOndemandServiceModel.fromJson(document.data()), + ); + } catch (e) { + print('FavouriteModel.getCurrencys Parse error $e'); + } + }); + + return lstFavourites; + } + + static Future> getCurrentProviderService( + FavouriteOndemandServiceModel model, + ) async { + List providerService = []; + + QuerySnapshot> reviewQuery = + await fireStore + .collection(CollectionName.providersServices) + .where('id', isEqualTo: model.service_id) + .where('sectionId', isEqualTo: model.section_id) + .get(); + await Future.forEach(reviewQuery.docs, ( + QueryDocumentSnapshot> document, + ) { + try { + providerService.add(ProviderServiceModel.fromJson(document.data())); + } catch (e) { + print( + 'FireStoreUtils.getReviewByProviderServiceId Parse error ${document.id} $e', + ); + } + }); + return providerService; + } +} diff --git a/lib/service/localization_service.dart b/lib/service/localization_service.dart new file mode 100644 index 0000000..b3040e7 --- /dev/null +++ b/lib/service/localization_service.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../lang/app_en.dart'; + +class LocalizationService extends Translations { + // Default locale + static const locale = Locale('en', 'US'); + + static final locales = [const Locale('en')]; + + // Keys and their translations + // Translations are separated maps in `lang` file + @override + Map> get keys => {'en_US': enUS}; + + // Gets locale from language, and updates the locale + void changeLocale(String lang) { + Get.updateLocale(Locale(lang)); + } +} diff --git a/lib/service/notification_service.dart b/lib/service/notification_service.dart new file mode 100644 index 0000000..bfa2600 --- /dev/null +++ b/lib/service/notification_service.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +Future firebaseMessageBackgroundHandle(RemoteMessage message) async { + log("BackGround Message :: ${message.messageId}"); +} + +class NotificationService { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + + Future initInfo() async { + await FirebaseMessaging.instance + .setForegroundNotificationPresentationOptions( + alert: true, + badge: true, + sound: true, + ); + var request = await FirebaseMessaging.instance.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + + if (request.authorizationStatus == AuthorizationStatus.authorized || + request.authorizationStatus == AuthorizationStatus.provisional) { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + var iosInitializationSettings = const DarwinInitializationSettings(); + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings, + onDidReceiveNotificationResponse: (payload) {}); + setupInteractedMessage(); + } + } + + Future setupInteractedMessage() async { + RemoteMessage? initialMessage = + await FirebaseMessaging.instance.getInitialMessage(); + if (initialMessage != null) { + FirebaseMessaging.onBackgroundMessage( + (message) => firebaseMessageBackgroundHandle(message)); + } + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + log("::::::::::::onMessage:::::::::::::::::"); + if (message.notification != null) { + log(message.notification.toString()); + display(message); + } + }); + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + log("::::::::::::onMessageOpenedApp:::::::::::::::::"); + if (message.notification != null) { + log(message.notification.toString()); + } + }); + log("::::::::::::Permission authorized:::::::::::::::::"); + await FirebaseMessaging.instance.subscribeToTopic("customer"); + } + + static Future? getToken() async { + String? token = await FirebaseMessaging.instance.getToken(); + return token; + } + + void display(RemoteMessage message) async { + log('Got a message whilst in the foreground!'); + log('Message data: ${message.notification!.body.toString()}'); + try { + AndroidNotificationChannel channel = const AndroidNotificationChannel( + '0', + 'goRide-customer', + description: 'Show QuickLAI Notification', + importance: Importance.max, + ); + AndroidNotificationDetails notificationDetails = + AndroidNotificationDetails(channel.id, channel.name, + channelDescription: 'your channel Description', + importance: Importance.high, + priority: Priority.high, + ticker: 'ticker'); + const DarwinNotificationDetails darwinNotificationDetails = + DarwinNotificationDetails( + presentAlert: true, presentBadge: true, presentSound: true); + NotificationDetails notificationDetailsBoth = NotificationDetails( + android: notificationDetails, iOS: darwinNotificationDetails); + await FlutterLocalNotificationsPlugin().show( + 0, + message.notification!.title, + message.notification!.body, + notificationDetailsBoth, + payload: jsonEncode(message.data), + ); + } on Exception catch (e) { + log(e.toString()); + } + } +} diff --git a/lib/service/send_notification.dart b/lib/service/send_notification.dart new file mode 100644 index 0000000..8f77bfe --- /dev/null +++ b/lib/service/send_notification.dart @@ -0,0 +1,108 @@ +import 'dart:convert'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/models/notification_model.dart'; +import 'package:customer/service/fire_store_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:http/http.dart' as http; +import 'package:googleapis_auth/auth_io.dart'; + +class SendNotification { + static final _scopes = ['https://www.googleapis.com/auth/firebase.messaging']; + + static Future getCharacters() { + return http.get(Uri.parse(Constant.jsonNotificationFileURL.toString())); + } + + static Future getAccessToken() async { + Map jsonData = {}; + + await getCharacters().then((response) { + jsonData = json.decode(response.body); + }); + final serviceAccountCredentials = ServiceAccountCredentials.fromJson(jsonData); + final client = await clientViaServiceAccount(serviceAccountCredentials, _scopes); + return client.credentials.accessToken.data; + } + + static Future sendFcmMessage(String type, String token, Map? payload) async { + print(type); + try { + final String accessToken = await getAccessToken(); + debugPrint("accessToken=======>"); + debugPrint(accessToken); + NotificationModel? notificationModel = await FireStoreUtils.getNotificationContent(type); + + final response = await http.post( + Uri.parse('https://fcm.googleapis.com/v1/projects/${Constant.senderId}/messages:send'), + headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken'}, + body: jsonEncode({ + 'message': { + 'token': token, + 'notification': {'body': notificationModel?.message ?? '', 'title': notificationModel?.subject ?? ''}, + 'data': payload, + }, + }), + ); + + debugPrint("Notification=======>"); + debugPrint(response.statusCode.toString()); + debugPrint(response.body); + return true; + } catch (e) { + debugPrint(e.toString()); + return false; + } + } + + static Future sendOneNotification({required String token, required String title, required String body, required Map payload}) async { + try { + final String accessToken = await getAccessToken(); + debugPrint("accessToken=======>"); + debugPrint(accessToken); + + final response = await http.post( + Uri.parse('https://fcm.googleapis.com/v1/projects/${Constant.senderId}/messages:send'), + headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken'}, + body: jsonEncode({ + 'message': { + 'token': token, + 'notification': {'body': body, 'title': title}, + 'data': payload, + }, + }), + ); + + debugPrint("Notification=======>"); + debugPrint(response.statusCode.toString()); + debugPrint(response.body); + return true; + } catch (e) { + debugPrint(e.toString()); + return false; + } + } + + static Future sendChatFcmMessage(String title, String message, String token, Map? payload) async { + try { + final String accessToken = await getAccessToken(); + final response = await http.post( + Uri.parse('https://fcm.googleapis.com/v1/projects/${Constant.senderId}/messages:send'), + headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken'}, + body: jsonEncode({ + 'message': { + 'token': token, + 'notification': {'body': message, 'title': title}, + 'data': payload, + }, + }), + ); + debugPrint("Notification=======>"); + debugPrint(response.statusCode.toString()); + debugPrint(response.body); + return true; + } catch (e) { + print(e); + return false; + } + } +} diff --git a/lib/themes/app_them_data.dart b/lib/themes/app_them_data.dart new file mode 100644 index 0000000..47dcaa8 --- /dev/null +++ b/lib/themes/app_them_data.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; + +class AppThemeData { + static const Color surface = Color(0xFFFFFFFF); + static const Color surfaceDark = Color(0xFF010309); + + // === Car-Rent (Light) === + static const Color carRent50 = Color(0xFFEBFAF2); + static const Color carRent100 = Color(0xFFB7EBCE); + static const Color carRent200 = Color(0xFF83DDAB); + static const Color carRent300 = Color(0xFF47CF88); + static const Color carRent400 = Color(0xFF37905E); + static const Color carRent500 = Color(0xFF1E5235); + static const Color carRent600 = Color(0xFF05140C); + + // === Car-Rent (Dark) === + static const Color carRentDark50 = Color(0xFF05140C); + static const Color carRentDark100 = Color(0xFF1E5235); + static const Color carRentDark200 = Color(0xFF37905E); + static const Color carRentDark300 = Color(0xFF4FCF88); + static const Color carRentDark400 = Color(0xFF83DDAB); + static const Color carRentDark500 = Color(0xFFB7EBCE); + static const Color carRentDark600 = Color(0xFFEBFAF2); + + // === E-Commerce (Light) === + static const Color ecommerce50 = Color(0xFFE5EDFF); + static const Color ecommerce100 = Color(0xFFABC4FF); + static const Color ecommerce200 = Color(0xFF72BCFF); + static Color ecommerce300 = const Color(0xFF3974FF); + static const Color ecommerce400 = Color(0xFF2650B2); + static const Color ecommerce500 = Color(0xFF132C66); + static const Color ecommerce600 = Color(0xFF00081A); + + // === E-Commerce (Dark) === + static const Color ecommerceDark50 = Color(0xFF00081A); + static const Color ecommerceDark100 = Color(0xFF132C66); + static const Color ecommerceDark200 = Color(0xFF2650B2); + static const Color ecommerceDark300 = Color(0xFF3974FF); + static const Color ecommerceDark400 = Color(0xFF729CFF); + static const Color ecommerceDark500 = Color(0xFFABC4FF); + static const Color ecommerceDark600 = Color(0xFFE5EDFF); + + // === Multi-Vendor === + static const Color multiVendor50 = Color(0xFFFFE6E6); + static const Color multiVendor100 = Color(0xFFFEB9B9); + static const Color multiVendor200 = Color(0xFFEB8B8B); + static const Color multiVendor300 = Color(0xFFFE5D5D); + static const Color multiVendor400 = Color(0xFFB13E3E); + static const Color multiVendor500 = Color(0xFF651F1F); + static const Color multiVendor600 = Color(0xFF910000); + + static const Color multiVendorDark50 = Color(0xFF910000); + static const Color multiVendorDark100 = Color(0xFF651F1F); + static const Color multiVendorDark200 = Color(0xFFB13E3E); + static const Color multiVendorDark300 = Color(0xFFFE5D5D); + static const Color multiVendorDark400 = Color(0xFFFE8B8B); + static const Color multiVendorDark500 = Color(0xFFFEB9B9); + static const Color multiVendorDark600 = Color(0xFFFFE6E6); + + // === On-Demand === + static const Color onDemand50 = Color(0xFFE6F8FF); + static const Color onDemand100 = Color(0xFF9DE5FF); + static const Color onDemand200 = Color(0xFF55D1FE); + static const Color onDemand300 = Color(0xFF0DBDFD); + static const Color onDemand400 = Color(0xFF0885B1); + static const Color onDemand500 = Color(0xFF044C65); + static const Color onDemand600 = Color(0xFF001319); + + static const Color onDemandDark50 = Color(0xFF001319); + static const Color onDemandDark100 = Color(0xFF004C65); + static const Color onDemandDark200 = Color(0xFF0885B1); + static const Color onDemandDark300 = Color(0xFF55D1FE); + static const Color onDemandDark400 = Color(0xFF55D1FE); + static const Color onDemandDark500 = Color(0xFF00A1F1); + static const Color onDemandDark600 = Color(0xFFE6F8FF); + + // === Taxi-Booking === + static const Color taxiBooking50 = Color(0xFFFFF6E5); + static const Color taxiBooking100 = Color(0xFFFFDFA8); + static const Color taxiBooking200 = Color(0xFFFFC06A); + static const Color taxiBooking300 = Color(0xFFFFB32C); + static const Color taxiBooking400 = Color(0xFFB27C1E); + static const Color taxiBooking500 = Color(0xFF66460F); + static const Color taxiBooking600 = Color(0xFF1A1000); + + static const Color taxiBookingDark50 = Color(0xFF1A1000); + static const Color taxiBookingDark100 = Color(0xFF1A1000); + static const Color taxiBookingDark200 = Color(0xFFFFC96A); + static const Color taxiBookingDark300 = Color(0xFFFFB32C); + static const Color taxiBookingDark400 = Color(0xFFFFC06A); + static const Color taxiBookingDark500 = Color(0xFFFFDFA8); + static const Color taxiBookingDark600 = Color(0xFFFFF6E5); + + // === Parcel-Service === + static const Color parcelService50 = Color(0xFFEAFBF3); + static const Color parcelService100 = Color(0xFFAAEFCF); + static const Color parcelService200 = Color(0xFF6AE2AB); + static const Color parcelService300 = Color(0xFF2AD587); + static const Color parcelService400 = Color(0xFF1E955E); + static const Color parcelService500 = Color(0xFF115536); + static const Color parcelService600 = Color(0xFF04150E); + + static const Color parcelServiceDark50 = Color(0xFF04150E); + static const Color parcelServiceDark100 = Color(0xFFAAEFCF); + static const Color parcelServiceDark200 = Color(0xFF1E955E); + static const Color parcelServiceDark300 = Color(0xFF2AD587); + static const Color parcelServiceDark400 = Color(0xFF6AE2AB); + static const Color parcelServiceDark500 = Color(0xFFAAEFCF); + static const Color parcelServiceDark600 = Color(0xFFEAFBF3); + + // === Grey === + static const Color grey50 = Color(0xFFF9FAFB); + static const Color grey100 = Color(0xFFF3F4F6); + static const Color grey200 = Color(0xFFE5E7EB); + static const Color grey300 = Color(0xFFD1D5DB); + static const Color grey400 = Color(0xFF9CA3AF); + static const Color grey500 = Color(0xFF6B7280); + static const Color grey600 = Color(0xFF4B5563); + static const Color grey700 = Color(0xFF374151); + static const Color grey800 = Color(0xFF1F2937); + static const Color grey900 = Color(0xFF0C111C); + + static const Color greyDark50 = Color(0xFF0C111C); + static const Color greyDark100 = Color(0xFF1F2937); + static const Color greyDark200 = Color(0xFF374151); + static const Color greyDark300 = Color(0xFF4B5563); + static const Color greyDark400 = Color(0xFF6B7280); + static const Color greyDark500 = Color(0xFF9CA3AF); + static const Color greyDark600 = Color(0xFFD1D5DB); + static const Color greyDark700 = Color(0xFFE5E7EB); + static const Color greyDark800 = Color(0xFFF3F4F6); + static const Color greyDark900 = Color(0xFFF9FAFB); + + // === Danger === + static const Color danger50 = Color(0xFFFFE5E6); + static const Color danger100 = Color(0xFFFFACAE); + static const Color danger200 = Color(0xFFFF7277); + static const Color danger300 = Color(0xFFFF3840); + static const Color danger400 = Color(0xFFB2262B); + static const Color danger500 = Color(0xFF661316); + static const Color danger600 = Color(0xFF1A0001); + + static const Color dangerDark50 = Color(0xFF1A0001); + static const Color dangerDark100 = Color(0xFF661316); + static const Color dangerDark200 = Color(0xFFB2262B); + static const Color dangerDark300 = Color(0xFFFF3840); + static const Color dangerDark400 = Color(0xFFFF7277); + static const Color dangerDark500 = Color(0xFFFFACAE); + static const Color dangerDark600 = Color(0xFFFFE5E6); + + // === Info === + static const Color info50 = Color(0xFFE5F9FF); + static const Color info100 = Color(0xFFACECFF); + static const Color info200 = Color(0xFF72DEFF); + static const Color info300 = Color(0xFF38D0FF); + static const Color info400 = Color(0xFF2692B2); + static const Color info500 = Color(0xFF135366); + static const Color info600 = Color(0xFF00141A); + + static const Color infoDark50 = Color(0xFF00141A); + static const Color infoDark100 = Color(0xFF135366); + static const Color infoDark200 = Color(0xFF2692B2); + static const Color infoDark300 = Color(0xFF38D0FF); + static const Color infoDark400 = Color(0xFF72DEFF); + static const Color infoDark500 = Color(0xFFACECFF); + static const Color infoDark600 = Color(0xFFE5F9FF); + + // === Success === + static const Color success50 = Color(0xFFE5FFEB); + static const Color success100 = Color(0xFFACFFBF); + static const Color success200 = Color(0xFF72FF93); + static const Color success300 = Color(0xFF38FF67); + static const Color success400 = Color(0xFF26B246); + static const Color success500 = Color(0xFF136626); + static const Color success600 = Color(0xFF001A06); + + static const Color successDark50 = Color(0xFF001A06); + static const Color successDark100 = Color(0xFF136626); + static const Color successDark200 = Color(0xFF26B246); + static const Color successDark300 = Color(0xFF38FF67); + static const Color successDark400 = Color(0xFF72FF93); + static const Color successDark500 = Color(0xFFACFFBF); + static const Color successDark600 = Color(0xFFE5FFEB); + + // === Warning === + static const Color warning50 = Color(0xFFFFF8E5); + static const Color warning100 = Color(0xFFFFE9AB); + static const Color warning200 = Color(0xFFFFDA72); + static const Color warning300 = Color(0xFFFFCB39); + static const Color warning400 = Color(0xFFB28D26); + static const Color warning500 = Color(0xFF665013); + static const Color warning600 = Color(0xFF191200); + + static const Color warningDark50 = Color(0xFF191200); + static const Color warningDark100 = Color(0xFF666655); + static const Color warningDark200 = Color(0xFFB28D26); + static const Color warningDark300 = Color(0xFFFFCB39); + static const Color warningDark400 = Color(0xFFFFDA72); + static const Color warningDark500 = Color(0xFFFFE9AB); + static const Color warningDark600 = Color(0xFFFFF8E5); + + static const Color primary50 = Color(0xFFFFEBE5); + static const Color primary100 = Color(0xFFFFC0AB); + static const Color primary200 = Color(0xFFFF9472); + static Color primary300 = const Color(0xFFFF6839); + static const Color primary400 = Color(0xFFB24826); + static const Color primary500 = Color(0xFF662713); + static const Color primary600 = Color(0xFF1A0600); + + static const String fontFamily = 'EssentialSans'; + + static const String black = 'EssentialSans'; + static const String bold = 'EssentialSans'; + static const String extraBold = 'EssentialSans'; + static const String extraLight = 'EssentialSans'; + static const String light = 'EssentialSans'; + static const String medium = 'EssentialSans'; + static const String regular = 'EssentialSans'; + static const String semiBold = 'EssentialSans'; + static const String thin = 'EssentialSans'; + + static TextStyle regularTextStyle({ + double? fontSize, + Color? color, + TextDecoration? decoration, + Color? decorationColor, + TextDecorationStyle? decorationStyle, + double? decorationThickness, + }) { + return TextStyle( + fontSize: fontSize ?? 14, + color: color ?? grey900, + fontFamily: fontFamily, + fontWeight: FontWeight.w400, + decoration: decoration ?? TextDecoration.none, + decorationColor: decorationColor ?? color ?? grey900, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + ); + } + + static TextStyle mediumTextStyle({ + double? fontSize, + Color? color, + TextDecoration? decoration, + Color? decorationColor, + TextDecorationStyle? decorationStyle, + double? decorationThickness, + }) { + return TextStyle( + fontSize: fontSize ?? 14, + color: color ?? grey900, + fontFamily: fontFamily, + fontWeight: FontWeight.w500, + decoration: decoration ?? TextDecoration.none, + decorationColor: decorationColor ?? color ?? grey900, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + ); + } + + static TextStyle semiBoldTextStyle({ + double? fontSize, + Color? color, + TextDecoration? decoration, + Color? decorationColor, + TextDecorationStyle? decorationStyle, + double? decorationThickness, + }) { + return TextStyle( + fontSize: fontSize ?? 14, + color: color ?? grey900, + fontFamily: fontFamily, + fontWeight: FontWeight.w600, + decoration: decoration ?? TextDecoration.none, + decorationColor: decorationColor ?? color ?? grey900, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + ); + } + + static TextStyle boldTextStyle({ + double? fontSize, + Color? color, + TextDecoration? decoration, + Color? decorationColor, + TextDecorationStyle? decorationStyle, + double? decorationThickness, + }) { + return TextStyle( + fontSize: fontSize ?? 22, + color: color ?? grey900, + fontFamily: fontFamily, + fontWeight: FontWeight.w700, + decoration: decoration ?? TextDecoration.none, + decorationColor: decorationColor ?? color ?? grey900, + decorationStyle: decorationStyle, + decorationThickness: decorationThickness, + ); + } +} diff --git a/lib/themes/custom_dialog_box.dart b/lib/themes/custom_dialog_box.dart new file mode 100644 index 0000000..6c87843 --- /dev/null +++ b/lib/themes/custom_dialog_box.dart @@ -0,0 +1,82 @@ +import 'package:customer/themes/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../controllers/theme_controller.dart'; +import 'app_them_data.dart'; + +class CustomDialogBox extends StatelessWidget { + final String title, descriptions, positiveString, negativeString; + final Widget? img; + final Function() positiveClick; + final Function() negativeClick; + + const CustomDialogBox({ + super.key, + required this.title, + required this.descriptions, + required this.img, + required this.positiveClick, + required this.negativeClick, + required this.positiveString, + required this.negativeString, + }); + + @override + Widget build(BuildContext context) { + final ThemeController themeController = Get.find(); + + return Obx(() { + final isDark = themeController.isDark.value; + + return Dialog(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), elevation: 0, backgroundColor: Colors.transparent, child: contentBox(context, isDark)); + }); + } + + Widget contentBox(BuildContext context, bool isDark) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(shape: BoxShape.rectangle, color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100, borderRadius: BorderRadius.circular(20)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + img ?? const SizedBox(), + const SizedBox(height: 20), + if (title.isNotEmpty) Text(title.tr, style: AppThemeData.boldTextStyle(fontSize: 20, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 5), + if (descriptions.isNotEmpty) + Text(descriptions.tr, textAlign: TextAlign.center, style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: InkWell( + onTap: negativeClick, + child: Container( + width: Responsive.width(100, context), + height: Responsive.height(5, context), + decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, borderRadius: BorderRadius.circular(200)), + child: Center( + child: Text(negativeString.tr, textAlign: TextAlign.center, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100)), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + onTap: positiveClick, + child: Container( + width: Responsive.width(100, context), + height: Responsive.height(5, context), + decoration: BoxDecoration(color: AppThemeData.success300, borderRadius: BorderRadius.circular(200)), + child: Center(child: Text('Confirm'.tr, textAlign: TextAlign.center, style: AppThemeData.mediumTextStyle(fontSize: 14, color: AppThemeData.grey100))), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/themes/easy_loading_config.dart b/lib/themes/easy_loading_config.dart new file mode 100644 index 0000000..a48db16 --- /dev/null +++ b/lib/themes/easy_loading_config.dart @@ -0,0 +1,21 @@ +import 'package:customer/themes/app_them_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import '../controllers/theme_controller.dart'; + +Future configEasyLoading() async { + final themeController = Get.find(); + + final isDark = themeController.isDark.value; + + EasyLoading.instance + ..indicatorType = EasyLoadingIndicatorType.fadingCircle + ..loadingStyle = EasyLoadingStyle.custom + ..backgroundColor = isDark ? AppThemeData.greyDark50 : AppThemeData.grey800 + ..indicatorColor = isDark ? Colors.white : Colors.white + ..textColor = isDark ? Colors.white : AppThemeData.greyDark900 + ..maskColor = Colors.black.withOpacity(0.5) + ..userInteractions = false + ..dismissOnTap = false; +} diff --git a/lib/themes/responsive.dart b/lib/themes/responsive.dart new file mode 100644 index 0000000..1a7bf4e --- /dev/null +++ b/lib/themes/responsive.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class Responsive { + static double width(double size, BuildContext context) { + return MediaQuery.of(context).size.width * (size / 100); + } + + static double height(double size, BuildContext context) { + return MediaQuery.of(context).size.height * (size / 100); + } +} diff --git a/lib/themes/round_button_border.dart b/lib/themes/round_button_border.dart new file mode 100644 index 0000000..f93be6e --- /dev/null +++ b/lib/themes/round_button_border.dart @@ -0,0 +1,76 @@ +import 'package:customer/controllers/theme_controller.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'app_them_data.dart'; + +class RoundedButtonBorder extends StatelessWidget { + final String title; + final double? width; + final double? height; + final double? fontSizes; + final double? radius; + final Color? color; + final Color? borderColor; + final Color? textColor; + final Widget? icon; + final bool? isRight; + final bool? isCenter; + final Function()? onPress; + + const RoundedButtonBorder({ + super.key, + required this.title, + this.height, + required this.onPress, + this.width, + this.radius, + this.color, + this.icon, + this.fontSizes, + this.textColor, + this.isRight, + this.borderColor, + this.isCenter, + }); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return InkWell( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + onPress?.call(); + }, + child: Container( + width: Responsive.width(width ?? 100, context), + height: Responsive.height(height ?? 6, context), + decoration: ShapeDecoration( + color: color ?? Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius ?? 50), side: BorderSide(color: borderColor ?? AppThemeData.danger300)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isRight == false) Padding(padding: const EdgeInsets.only(right: 10, left: 0), child: icon), + isCenter == true + ? Text(title.tr, textAlign: TextAlign.center, style: AppThemeData.semiBoldTextStyle(fontSize: fontSizes ?? 14, color: textColor ?? AppThemeData.grey800)) + : Expanded( + child: Padding( + padding: EdgeInsets.only(right: isRight == null ? 0 : 30), + child: Text( + title.tr, + textAlign: TextAlign.center, + style: AppThemeData.semiBoldTextStyle(fontSize: fontSizes ?? 14, color: textColor ?? (isDark ? AppThemeData.grey100 : AppThemeData.grey700)), + ), + ), + ), + if (isRight == true) Padding(padding: const EdgeInsets.only(left: 10, right: 20), child: icon), + ], + ), + ), + ); + } +} diff --git a/lib/themes/round_button_fill.dart b/lib/themes/round_button_fill.dart new file mode 100644 index 0000000..eada310 --- /dev/null +++ b/lib/themes/round_button_fill.dart @@ -0,0 +1,67 @@ +import 'package:customer/themes/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'app_them_data.dart'; + +class RoundedButtonFill extends StatelessWidget { + final String title; + final double? width; + final double? height; + final double? fontSizes; + final double? borderRadius; + final Color? color; + final Color? textColor; + final Widget? icon; + final bool? isRight; + final bool? isCenter; + final VoidCallback? onPress; + + // final Function()? onPress; + + const RoundedButtonFill({ + super.key, + required this.title, + this.borderRadius, + this.height, + required this.onPress, + this.width, + this.color, + this.isCenter, + this.icon, + this.fontSizes, + this.textColor, + this.isRight, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(borderRadius ?? 50), + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + onPress?.call(); + }, + child: Container( + width: Responsive.width(width ?? 100, context), + height: Responsive.height(height ?? 6, context), + decoration: ShapeDecoration(color: color, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius ?? 50))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isRight == false) Padding(padding: const EdgeInsets.only(right: 10, left: 10), child: icon), + isCenter == true + ? Text(title.tr, textAlign: TextAlign.center, style: AppThemeData.semiBoldTextStyle(fontSize: fontSizes ?? 16, color: textColor ?? AppThemeData.grey50)) + : Expanded( + child: Padding( + padding: EdgeInsets.only(right: isRight == null ? 0 : 30), + child: Text(title.tr, textAlign: TextAlign.center, style: AppThemeData.semiBoldTextStyle(fontSize: fontSizes ?? 16, color: textColor ?? AppThemeData.grey50)), + ), + ), + if (isRight == true) Padding(padding: const EdgeInsets.only(left: 10, right: 10), child: icon), + ], + ), + ), + ); + } +} diff --git a/lib/themes/show_toast_dialog.dart b/lib/themes/show_toast_dialog.dart new file mode 100644 index 0000000..983effb --- /dev/null +++ b/lib/themes/show_toast_dialog.dart @@ -0,0 +1,26 @@ +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +class ShowToastDialog { + /// Show a toast message with customizable position. + static void showToast( + String? message, { + EasyLoadingToastPosition position = EasyLoadingToastPosition.top, + }) { + if (message == null || message.isEmpty) return; + EasyLoading.showToast(message, toastPosition: position); + } + + /// Show a loading indicator with a status message. + static void showLoader(String message) { + EasyLoading.show( + status: message, + dismissOnTap: false, + maskType: EasyLoadingMaskType.clear, + ); + } + + /// Dismiss any active loading indicator. + static void closeLoader() { + EasyLoading.dismiss(); + } +} diff --git a/lib/themes/styles.dart b/lib/themes/styles.dart new file mode 100644 index 0000000..2ac2a47 --- /dev/null +++ b/lib/themes/styles.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'app_them_data.dart'; + +class Styles { + static ThemeData themeData(bool isDarkTheme, BuildContext context) { + return ThemeData( + scaffoldBackgroundColor: isDarkTheme ? AppThemeData.surfaceDark : AppThemeData.surface, + primaryColor: isDarkTheme ? AppThemeData.dangerDark300 : AppThemeData.danger300, + brightness: isDarkTheme ? Brightness.dark : Brightness.light, + appBarTheme: AppBarTheme( + backgroundColor: isDarkTheme ? AppThemeData.grey900 : AppThemeData.grey50, + ), + timePickerTheme: TimePickerThemeData( + backgroundColor: isDarkTheme ? AppThemeData.greyDark100 : AppThemeData.grey100, + dialTextStyle: TextStyle(fontWeight: FontWeight.bold, color: isDarkTheme ? AppThemeData.greyDark100 : AppThemeData.grey100), + dialTextColor: isDarkTheme ? AppThemeData.greyDark50 : AppThemeData.grey50, + hourMinuteTextColor: isDarkTheme ? AppThemeData.greyDark100 : AppThemeData.grey100, + dayPeriodTextColor: isDarkTheme ? AppThemeData.greyDark100 : AppThemeData.grey100, + ), + ); + } +} diff --git a/lib/themes/text_field_widget.dart b/lib/themes/text_field_widget.dart new file mode 100644 index 0000000..8e0b58c --- /dev/null +++ b/lib/themes/text_field_widget.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import '../controllers/theme_controller.dart'; +import 'app_them_data.dart'; + +class TextFieldWidget extends StatefulWidget { + final String? title; + final String hintText; + final TextEditingController? controller; + final Widget? prefix; + final Widget? suffix; + final bool? enable; + final bool? readOnly; + final bool? obscureText; + final int? maxLine; + final TextInputType? textInputType; + final List? inputFormatters; + final ValueChanged? onchange; + final TextInputAction? textInputAction; + final FocusNode? focusNode; + final ValueChanged? onFieldSubmitted; + final Color? hintColor; + final Color? backgroundColor; + final Color? borderColor; + + const TextFieldWidget({ + super.key, + this.textInputType, + this.enable, + this.readOnly, + this.obscureText, + this.prefix, + this.suffix, + this.title, + required this.hintText, + required this.controller, + this.maxLine, + this.inputFormatters, + this.onchange, + this.textInputAction, + this.focusNode, + this.onFieldSubmitted, + this.borderColor, + this.hintColor, + this.backgroundColor, + }); + + @override + State createState() => _TextFieldWidgetState(); +} + +class _TextFieldWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = widget.focusNode ?? FocusNode(); + _focusNode.addListener(() { + setState(() {}); + }); + } + + // @override + // void dispose() { + // if (widget.focusNode == null) { + // _focusNode.dispose(); + // } + // super.dispose(); + // } + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + + final borderColor = widget.borderColor ?? (_focusNode.hasFocus ? (isDark ? AppThemeData.greyDark400 : AppThemeData.grey400) : (isDark ? AppThemeData.greyDark200 : AppThemeData.grey200)); + + final fillColor = + widget.backgroundColor ?? (isDark ? (_focusNode.hasFocus ? AppThemeData.greyDark50 : AppThemeData.greyDark100) : (_focusNode.hasFocus ? AppThemeData.grey100 : Colors.transparent)); + + final textColor = isDark ? AppThemeData.greyDark900 : AppThemeData.grey900; + + final hintColor = widget.hintColor ?? (isDark ? AppThemeData.grey400 : AppThemeData.greyDark400); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title != null) ...[ + Text(widget.title!.tr, style: AppThemeData.boldTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark800 : AppThemeData.grey800)), + const SizedBox(height: 5), + ], + TextFormField( + keyboardType: widget.textInputType ?? TextInputType.text, + textCapitalization: TextCapitalization.sentences, + controller: widget.controller, + maxLines: widget.maxLine ?? 1, + focusNode: _focusNode, + textInputAction: widget.textInputAction ?? TextInputAction.done, + inputFormatters: widget.inputFormatters, + obscureText: widget.obscureText ?? false, + obscuringCharacter: '●', + onChanged: widget.onchange, + readOnly: widget.readOnly ?? false, + onFieldSubmitted: widget.onFieldSubmitted, + style: AppThemeData.semiBoldTextStyle(color: textColor), + decoration: InputDecoration( + errorStyle: const TextStyle(color: Colors.red), + filled: true, + enabled: widget.enable ?? true, + fillColor: fillColor, + contentPadding: EdgeInsets.symmetric(vertical: widget.title == null ? 15 : (widget.enable == false ? 13 : 8), horizontal: 10), + prefixIcon: widget.prefix, + suffixIcon: widget.suffix, + prefixIconConstraints: const BoxConstraints(minHeight: 20, minWidth: 20), + suffixIconConstraints: const BoxConstraints(minHeight: 20, minWidth: 20), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: borderColor)), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: borderColor)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: borderColor, width: 1.2)), + errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Colors.red)), + disabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: borderColor)), + hintText: widget.hintText.tr, + hintStyle: AppThemeData.regularTextStyle(fontSize: 14, color: hintColor), + ), + ), + ], + ); + } +} diff --git a/lib/utils/network_image_widget.dart b/lib/utils/network_image_widget.dart new file mode 100644 index 0000000..d032ca5 --- /dev/null +++ b/lib/utils/network_image_widget.dart @@ -0,0 +1,29 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import '../constant/constant.dart'; + +class NetworkImageWidget extends StatelessWidget { + final String imageUrl; + final double? height; + final double? width; + final Widget? errorWidget; + final BoxFit? fit; + final double? borderRadius; + final Color? color; + + const NetworkImageWidget({super.key, this.height, this.width, this.fit, required this.imageUrl, this.borderRadius, this.errorWidget, this.color}); + + @override + Widget build(BuildContext context) { + return CachedNetworkImage( + imageUrl: imageUrl, + fit: fit ?? BoxFit.fitWidth, + height: height, + width: width, + color: color, + progressIndicatorBuilder: + (context, url, downloadProgress) => Center(child: SizedBox(height: 24, width: 24, child: CircularProgressIndicator(value: downloadProgress.progress))), + errorWidget: (context, url, error) => errorWidget ?? Image.network(Constant.placeHolderImage, fit: fit ?? BoxFit.fitWidth, height: height, width: width), + ); + } +} diff --git a/lib/utils/notification_service.dart b/lib/utils/notification_service.dart new file mode 100644 index 0000000..1832fcb --- /dev/null +++ b/lib/utils/notification_service.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +Future firebaseMessageBackgroundHandle(RemoteMessage message) async { + log("BackGround Message :: ${message.messageId}"); + // NotificationService.redirectScreen(message); +} + +class NotificationService { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + Future initInfo() async { + await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true); + var request = await FirebaseMessaging.instance.requestPermission(alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true); + + if (request.authorizationStatus == AuthorizationStatus.authorized || request.authorizationStatus == AuthorizationStatus.provisional) { + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); + var iosInitializationSettings = const DarwinInitializationSettings(); + final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (payload) {}); + setupInteractedMessage(); + } + } + + Future setupInteractedMessage() async { + RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + if (initialMessage != null) { + FirebaseMessaging.onBackgroundMessage((message) => firebaseMessageBackgroundHandle(message)); + } + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + log("::::::::::::onMessage:::::::::::::::::"); + if (message.notification != null) { + log(message.notification.toString()); + // display(message); + } + }); + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async { + log("::::::::::::onMessageOpenedApp:::::::::::::::::"); + if (message.notification != null) { + log(message.notification.toString()); + // redirectScreen(message); + } + }); + log("::::::::::::Permission authorized:::::::::::::::::"); + await FirebaseMessaging.instance.subscribeToTopic("customer"); + } + + static Future getToken() async { + String? token = await FirebaseMessaging.instance.getToken(); + return token!; + } + + // static redirectScreen(RemoteMessage message) async { + // Map data = message.data; + // if (data['type'] == "user_chat") { + // String senderId = data['senderId']; + // String receiverId = data['receiverId']; + // + // ShowToastDialog.showLoader("Please wait".tr); + // UserModel? senderUserModel = await FireStoreUtils.getUserProfile(senderId); + // UserModel? receiverUserModel = await FireStoreUtils.getUserProfile(receiverId); + // ShowToastDialog.closeLoader(); + // bool isMe = senderUserModel!.id == senderId; + // Get.to(const UserChatScreen(), arguments: {"receiverModel": isMe ? senderUserModel : receiverUserModel}); + // } else if (data['type'] == "project_chat") { + // String isSender = data['isSender']; + // String businessId = data['businessId']; + // String projectId = data['projectId']; + // + // ShowToastDialog.showLoader("Please wait".tr); + // PricingRequestModel? pricingRequestModel = await FireStoreUtils.getPricingRequestById(projectId); + // BusinessModel? businessModel = await FireStoreUtils.getBusinessById(businessId); + // UserModel? userModel = await FireStoreUtils.getUserProfile(pricingRequestModel!.userId.toString()); + // ShowToastDialog.closeLoader(); + // Get.to(ChatScreen(), arguments: { + // "userModel": userModel!, + // "businessModel": businessModel!, + // "projectModel": pricingRequestModel, + // "isSender": isSender == "business" ? "user" : "business", + // }); + // } else if (data['type'] == "project_request") { + // String businessId = data['businessId']; + // String projectId = data['projectId']; + // BusinessModel? businessModel = await FireStoreUtils.getBusinessById(businessId); + // Get.to(BusinessProjectListScreen(), arguments: {"businessModel": businessModel}); + // } else if (data['type'] == "review") { + // String businessId = data['businessId']; + // BusinessModel? businessModel = await FireStoreUtils.getBusinessById(businessId); + // Get.to(BusinessDetailsScreen(), arguments: {"businessModel": businessModel}); + // } else if (data['type'] == "user_follow") { + // String userId = data['userId']; + // ShowToastDialog.showLoader("Please wait"); + // UserModel? userModel0 = await FireStoreUtils.getUserProfile(userId.toString()); + // ShowToastDialog.closeLoader(); + // Get.to(OtherPeopleScreen(), arguments: {"userModel": userModel0}); + // } + // } + + void display(RemoteMessage message) async { + log('Got a message whilst in the foreground!'); + log('Message data: ${message.notification!.body.toString()}'); + try { + AndroidNotificationChannel channel = const AndroidNotificationChannel('0', 'eMart customer', description: 'Show eMart Notification', importance: Importance.max); + AndroidNotificationDetails notificationDetails = AndroidNotificationDetails( + channel.id, + channel.name, + channelDescription: 'your channel Description', + importance: Importance.high, + priority: Priority.high, + ticker: 'ticker', + ); + const DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails(presentAlert: true, presentBadge: true, presentSound: true); + NotificationDetails notificationDetailsBoth = NotificationDetails(android: notificationDetails, iOS: darwinNotificationDetails); + await FlutterLocalNotificationsPlugin().show(0, message.notification!.title, message.notification!.body, notificationDetailsBoth, payload: jsonEncode(message.data)); + } on Exception catch (e) { + log(e.toString()); + } + } +} diff --git a/lib/utils/preferences.dart b/lib/utils/preferences.dart new file mode 100644 index 0000000..3cf16e7 --- /dev/null +++ b/lib/utils/preferences.dart @@ -0,0 +1,70 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class Preferences { + static const isFinishOnBoardingKey = "isFinishOnBoardingKey"; + static const isLogin = "isLogin"; + static const accessToken = "accessToken"; + static const userData = "userData"; + static const themKey = "themKey"; + static const languageCodeKey = 'languageCodeKey'; + static const zipcode = 'zipcode'; + static const foodDeliveryType = "foodDeliveryType"; + static const payFastSettings = "payFastSettings"; + static const mercadoPago = "MercadoPago"; + static const paypalSettings = "paypalSettings"; + static const stripeSettings = "stripeSettings"; + static const flutterWave = "flutterWave"; + static const payStack = "payStack"; + static const paytmSettings = "PaytmSettings"; + static const walletSettings = "walletSettings"; + static const razorpaySettings = "razorpaySettings"; + static const codSettings = "CODSettings"; + static const midTransSettings = "midTransSettings"; + static const orangeMoneySettings = "orangeMoneySettings"; + static const xenditSettings = "xenditSettings"; + + static late SharedPreferences pref; + + static Future initPref() async { + pref = await SharedPreferences.getInstance(); + } + + /// Get boolean safely, fallback if stored value is string + static bool getBoolean(String key) { + final value = pref.get(key); + if (value is bool) return value; + if (value is String) { + // fallback for old string "Dark"/"Light" + return value.toLowerCase() == "dark"; + } + return false; + } + + static Future setBoolean(String key, bool value) async { + await pref.setBool(key, value); + } + + static String getString(String key, {String? defaultValue}) { + return pref.getString(key) ?? defaultValue ?? ""; + } + + static Future setString(String key, String value) async { + await pref.setString(key, value); + } + + static int getInt(String key) { + return pref.getInt(key) ?? 0; + } + + static Future setInt(String key, int value) async { + await pref.setInt(key, value); + } + + static Future clearSharPreference() async { + await pref.clear(); + } + + static Future clearKeyData(String key) async { + await pref.remove(key); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..3e2b574 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,125 @@ +import 'package:customer/constant/constant.dart'; +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get_utils/src/extensions/internacionalization.dart'; +import 'package:map_launcher/map_launcher.dart'; +import '../themes/show_toast_dialog.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:location/location.dart' as loc; + +class Utils { + static Future getCurrentLocation() async { + bool serviceEnabled; + LocationPermission permission; + + // Test if location services are enabled. + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + // Location services are not enabled don't continue + // accessing the position and request users of the + // App to enable the location services. + await loc.Location().requestService(); + return null; + } + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + // Permissions are denied, next time you could try + // requesting permissions again (this is also where + // Android's shouldShowRequestPermissionRationale + // returned true. According to Android guidelines + // your App should show an explanatory UI now. + return null; + } + } + + if (permission == LocationPermission.deniedForever) { + // Permissions are denied forever, handle appropriately. + return Future.error('Location permissions are permanently denied, we cannot request permissions.'); + } + + // When we reach here, permissions are granted and we can + // continue accessing the position of the device. + return await Geolocator.getCurrentPosition(); + } + + static Future getAddressFromCoordinates(double lat, double lng) async { + try { + List placemarks = await placemarkFromCoordinates(lat, lng); + if (placemarks.isNotEmpty) { + Placemark place = placemarks.first; + String address = "${place.name ?? ''}, ${place.subLocality ?? ''}, ${place.locality ?? ''}, ${place.administrativeArea ?? ''}, ${place.country ?? ''}"; + return address; + } + return "Unknown location"; + } catch (e) { + return "Unknown location"; + } + } + + static Future redirectMap({required String name, required double latitude, required double longLatitude}) async { + if (Constant.mapType == "google") { + bool? isAvailable = await MapLauncher.isMapAvailable(MapType.google); + if (isAvailable == true) { + await MapLauncher.showDirections(mapType: MapType.google, directionsMode: DirectionsMode.driving, destinationTitle: name, destination: Coords(latitude, longLatitude)); + } else { + ShowToastDialog.showToast("Google map is not installed".tr); + } + } else if (Constant.mapType == "googleGo") { + bool? isAvailable = await MapLauncher.isMapAvailable(MapType.googleGo); + if (isAvailable == true) { + await MapLauncher.showDirections(mapType: MapType.googleGo, directionsMode: DirectionsMode.driving, destinationTitle: name, destination: Coords(latitude, longLatitude)); + } else { + ShowToastDialog.showToast("Google Go map is not installed".tr); + } + } else if (Constant.mapType == "waze") { + bool? isAvailable = await MapLauncher.isMapAvailable(MapType.waze); + if (isAvailable == true) { + await MapLauncher.showDirections(mapType: MapType.waze, directionsMode: DirectionsMode.driving, destinationTitle: name, destination: Coords(latitude, longLatitude)); + } else { + ShowToastDialog.showToast("Waze is not installed".tr); + } + } else if (Constant.mapType == "mapswithme") { + bool? isAvailable = await MapLauncher.isMapAvailable(MapType.mapswithme); + if (isAvailable == true) { + await MapLauncher.showDirections(mapType: MapType.mapswithme, directionsMode: DirectionsMode.driving, destinationTitle: name, destination: Coords(latitude, longLatitude)); + } else { + ShowToastDialog.showToast("Mapswithme is not installed".tr); + } + } else if (Constant.mapType == "yandexNavi") { + bool? isAvailable = await MapLauncher.isMapAvailable(MapType.yandexNavi); + if (isAvailable == true) { + await MapLauncher.showDirections(mapType: MapType.yandexNavi, directionsMode: DirectionsMode.driving, destinationTitle: name, destination: Coords(latitude, longLatitude)); + } else { + ShowToastDialog.showToast("YandexNavi is not installed".tr); + } + } else if (Constant.mapType == "yandexMaps") { + bool? isAvailable = await MapLauncher.isMapAvailable(MapType.yandexMaps); + if (isAvailable == true) { + await MapLauncher.showDirections(mapType: MapType.yandexMaps, directionsMode: DirectionsMode.driving, destinationTitle: name, destination: Coords(latitude, longLatitude)); + } else { + ShowToastDialog.showToast("yandexMaps map is not installed".tr); + } + } + } + + static String formatAddress({required SelectedLocationModel selectedLocation}) { + List parts = []; + + if (selectedLocation.address!.name != null && selectedLocation.address!.name!.isNotEmpty) parts.add(selectedLocation.address!.name!); + if (selectedLocation.address!.subThoroughfare != null && selectedLocation.address!.subThoroughfare!.isNotEmpty) parts.add(selectedLocation.address!.subThoroughfare!); + if (selectedLocation.address!.thoroughfare != null && selectedLocation.address!.thoroughfare!.isNotEmpty) parts.add(selectedLocation.address!.thoroughfare!); + if (selectedLocation.address!.subLocality != null && selectedLocation.address!.subLocality!.isNotEmpty) parts.add(selectedLocation.address!.subLocality!); + if (selectedLocation.address!.locality != null && selectedLocation.address!.locality!.isNotEmpty) parts.add(selectedLocation.address!.locality!); + if (selectedLocation.address!.subAdministrativeArea != null && selectedLocation.address!.subAdministrativeArea!.isNotEmpty) { + parts.add(selectedLocation.address!.subAdministrativeArea!); + } + if (selectedLocation.address!.administrativeArea != null && selectedLocation.address!.administrativeArea!.isNotEmpty) parts.add(selectedLocation.address!.administrativeArea!); + if (selectedLocation.address!.postalCode != null && selectedLocation.address!.postalCode!.isNotEmpty) parts.add(selectedLocation.address!.postalCode!); + if (selectedLocation.address!.country != null && selectedLocation.address!.country!.isNotEmpty) parts.add(selectedLocation.address!.country!); + if (selectedLocation.address!.isoCountryCode != null && selectedLocation.address!.isoCountryCode!.isNotEmpty) parts.add(selectedLocation.address!.isoCountryCode!); + + return parts.join(', '); + } +} diff --git a/lib/widget/firebase_pagination/firebase_pagination.dart b/lib/widget/firebase_pagination/firebase_pagination.dart new file mode 100644 index 0000000..1d21b9a --- /dev/null +++ b/lib/widget/firebase_pagination/firebase_pagination.dart @@ -0,0 +1,13 @@ +/// A package to paginate your firebase related data with realtime updates. +/// +/// It can be used for `Firestore` and `Firebase Realtime Database`. +/// +/// Data can be shown in `list`, `grid` and `scrollable wrap` view. +library; + +export 'src/firestore_pagination.dart'; +// Data Models +export 'src/models/view_type.dart'; +export 'src/models/wrap_options.dart'; +// Widgets +export 'src/realtime_db_pagination.dart'; diff --git a/lib/widget/firebase_pagination/src/firestore_pagination.dart b/lib/widget/firebase_pagination/src/firestore_pagination.dart new file mode 100644 index 0000000..e0e6e8b --- /dev/null +++ b/lib/widget/firebase_pagination/src/firestore_pagination.dart @@ -0,0 +1,335 @@ +// Dart Packages +import 'dart:async'; + +// Firebase Packages +import 'package:cloud_firestore/cloud_firestore.dart'; +// Flutter Packages +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +// Functions +import 'functions/separator_builder.dart'; +// Data Models +import 'models/page_options.dart'; +import 'models/view_type.dart'; +import 'models/wrap_options.dart'; +// Widgets +import 'widgets/defaults/bottom_loader.dart'; +import 'widgets/defaults/empty_screen.dart'; +import 'widgets/defaults/initial_loader.dart'; +import 'widgets/views/build_pagination.dart'; + +/// A [StreamBuilder] that automatically loads more data when the user scrolls +/// to the bottom. +/// +/// Optimized for [FirebaseFirestore] with fields like `createdAt` and +/// `timestamp` to sort the data. +/// +/// Supports live updates and realtime updates to loaded data. +/// +/// Data can be represented in a [ListView], [GridView] or scollable [Wrap]. +class FirestorePagination extends StatefulWidget { + /// Creates a [StreamBuilder] widget that automatically loads more data when + /// the user scrolls to the bottom. + /// + /// Optimized for [FirebaseFirestore] with fields like `createdAt` and + /// `timestamp` to sort the data. + /// + /// Supports live updates and realtime updates to loaded data. + /// + /// Data can be represented in a [ListView], [GridView] or scollable [Wrap]. + const FirestorePagination({ + required this.query, + required this.itemBuilder, + super.key, + this.separatorBuilder, + this.limit = 10, + this.viewType = ViewType.list, + this.isLive = false, + this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + this.wrapOptions = const WrapOptions(), + this.pageOptions = const PageOptions(), + this.onEmpty = const EmptyScreen(), + this.bottomLoader = const BottomLoader(), + this.initialLoader = const InitialLoader(), + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.shrinkWrap = false, + this.physics, + this.padding, + this.controller, + this.pageController, + }); + + /// The query to use to fetch data from Firestore. + /// + /// ### Note: + /// - The query must **NOT** contain a `limit` itself. + /// - The `limit` must be set using the [limit] property of this widget. + final Query query; + + /// The builder to use to build the items in the list. + /// + /// The builder is passed the build context, snapshot of the document and + /// index of the item in the list. + final Widget Function(BuildContext, List, int) itemBuilder; + + /// The builder to use to render the separator. + /// + /// Only used if [viewType] is [ViewType.list]. + /// + /// Default [Widget] is [SizedBox.shrink]. + final Widget Function(BuildContext, int)? separatorBuilder; + + /// The number of items to fetch from Firestore at once. + /// + /// Defaults to `10`. + final int limit; + + /// The type of view to use for the list. + /// + /// Defaults to [ViewType.list]. + final ViewType viewType; + + /// Whether to fetch newly added items as they are added to Firestore. + /// + /// Defaults to `false`. + final bool isLive; + + /// The delegate to use for the [GridView]. + /// + /// Defaults to [SliverGridDelegateWithFixedCrossAxisCount]. + final SliverGridDelegate gridDelegate; + + /// The [Wrap] widget properties to use. + /// + /// Defaults to [WrapOptions]. + final WrapOptions wrapOptions; + + /// The [PageView] properties to use. + /// + /// Defaults to [PageOptions]. + final PageOptions pageOptions; + + /// The widget to use when data is empty. + /// + /// Defaults to [EmptyScreen]. + final Widget onEmpty; + + /// The widget to use when more data is loading. + /// + /// Defaults to [BottomLoader]. + final Widget bottomLoader; + + /// The widget to use when data is loading initially. + /// + /// Defaults to [InitialLoader]. + final Widget initialLoader; + + /// The scrolling direction of the [ScrollView]. + final Axis scrollDirection; + + /// Whether the [ScrollView] scrolls in the reading direction. + final bool reverse; + + /// Should the [ScrollView] be shrink-wrapped. + final bool shrinkWrap; + + /// The scroll behavior to use for the [ScrollView]. + final ScrollPhysics? physics; + + /// The padding to use for the [ScrollView]. + final EdgeInsetsGeometry? padding; + + /// The scroll controller to use for the [ScrollView]. + /// + /// Defaults to [ScrollController]. + final ScrollController? controller; + + /// The page controller to use for the [PageView]. + /// + /// Defaults to [PageController]. + final PageController? pageController; + + @override + State createState() => _FirestorePaginationState(); +} + +/// The state of the [FirestorePagination] widget. +class _FirestorePaginationState extends State { + /// All the data that has been loaded from Firestore. + final List _docs = []; + + /// Snapshot subscription for the query. + /// + /// Also handles updates to loaded data. + StreamSubscription? _streamSub; + + /// Snapshot subscription for the query to handle newly added data. + StreamSubscription? _liveStreamSub; + + /// [ScrollController] to listen to scroll end and load more data. + late final ScrollController _controller = widget.controller ?? ScrollController(); + + /// [PageController] to listen to page changes and load more data. + late final PageController _pageController = widget.pageController ?? PageController(); + + /// Whether initial data is loading. + bool _isInitialLoading = true; + + /// Whether more data is loading. + bool _isFetching = false; + + /// Whether the end for given query has been reached. + /// + /// This is used to determine if more data should be loaded when the user + /// scrolls to the bottom. + bool _isEnded = false; + + /// Loads more data from Firestore and handles updates to loaded data. + /// + /// Setting [getMore] to `false` will only set listener for the currently + /// loaded data. + Future _loadDocuments({bool getMore = true}) async { + // To cancel previous updates listener when new one is set. + final tempSub = _streamSub; + + if (getMore) setState(() => _isFetching = true); + + final docsLimit = _docs.length + (getMore ? widget.limit : 0); + var docsQuery = widget.query.limit(docsLimit); + if (_docs.isNotEmpty) { + docsQuery = docsQuery.startAtDocument(_docs.first); + } + + _streamSub = docsQuery.snapshots().listen((QuerySnapshot snapshot) async { + await tempSub?.cancel(); + + _docs + ..clear() + ..addAll(snapshot.docs); + + // To set new updates listener for the existing data + // or to set new live listener if the first document is removed. + final isDocRemoved = snapshot.docChanges.any( + (DocumentChange change) => change.type == DocumentChangeType.removed, + ); + + _isFetching = false; + if (!isDocRemoved) { + _isEnded = snapshot.docs.length < docsLimit; + } + + if (isDocRemoved || _isInitialLoading) { + _isInitialLoading = false; + if (snapshot.docs.isNotEmpty) { + // Set updates listener for the existing data starting from the first + // document only. + await _loadDocuments(getMore: false); + } else { + _streamSub?.cancel(); + } + if (widget.isLive) _setLiveListener(); + } + + if (mounted) setState(() {}); + + // Add data till the view is scrollable. This ensures that the user can + // scroll to the bottom and load more data. + if (_isInitialLoading || _isFetching || _isEnded) return; + SchedulerBinding.instance.addPostFrameCallback((_) { + if (_controller.hasClients && _controller.position.maxScrollExtent <= 0) { + _loadDocuments(); + } + }); + }); + } + + /// Sets the live listener for the query. + /// + /// Fires when new data is added to the query. + Future _setLiveListener() async { + // To cancel previous live listener when new one is set. + final tempSub = _liveStreamSub; + + var latestDocQuery = widget.query.limit(1); + if (_docs.isNotEmpty) { + latestDocQuery = latestDocQuery.endBeforeDocument(_docs.first); + } + + _liveStreamSub = latestDocQuery.snapshots(includeMetadataChanges: true).listen( + (QuerySnapshot snapshot) async { + await tempSub?.cancel(); + if (snapshot.docs.isEmpty || snapshot.docs.first.metadata.hasPendingWrites) return; + + _docs.insert(0, snapshot.docs.first); + + // To handle newly added data after this curently loaded data. + await _setLiveListener(); + + // Set updates listener for the newly added data. + _loadDocuments(getMore: false); + }, + ); + } + + /// To handle scroll end event and load more data. + void _scrollListener() { + if (_isInitialLoading || _isFetching || _isEnded) return; + if (!_controller.hasClients) return; + + final position = _controller.position; + if (position.pixels >= (position.maxScrollExtent - 50)) { + _loadDocuments(); + } + } + + @override + void initState() { + super.initState(); + _loadDocuments(); + _controller.addListener(_scrollListener); + } + + @override + void dispose() { + _streamSub?.cancel(); + _liveStreamSub?.cancel(); + _controller + ..removeListener(_scrollListener) + ..dispose(); + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _isInitialLoading + ? widget.initialLoader + : _docs.isEmpty + ? widget.onEmpty + : BuildPagination( + items: _docs, + itemBuilder: widget.itemBuilder, + separatorBuilder: widget.separatorBuilder ?? separatorBuilder, + isLoading: _isFetching, + viewType: widget.viewType, + bottomLoader: widget.bottomLoader, + gridDelegate: widget.gridDelegate, + wrapOptions: widget.wrapOptions, + pageOptions: widget.pageOptions, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: _controller, + pageController: _pageController, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + padding: widget.padding, + onPageChanged: (index) { + if (index >= _docs.length - 1) _loadDocuments(); + }, + ); + } +} diff --git a/lib/widget/firebase_pagination/src/functions/separator_builder.dart b/lib/widget/firebase_pagination/src/functions/separator_builder.dart new file mode 100644 index 0000000..f94930c --- /dev/null +++ b/lib/widget/firebase_pagination/src/functions/separator_builder.dart @@ -0,0 +1,7 @@ +// Flutter Packages +import 'package:flutter/widgets.dart'; + +/// Returns a [Widget] to be render as separator in a [ListView]. +Widget separatorBuilder(BuildContext context, int index) { + return const SizedBox.shrink(); +} diff --git a/lib/widget/firebase_pagination/src/models/page_options.dart b/lib/widget/firebase_pagination/src/models/page_options.dart new file mode 100644 index 0000000..64c0d47 --- /dev/null +++ b/lib/widget/firebase_pagination/src/models/page_options.dart @@ -0,0 +1,69 @@ +// Flutter Packages +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +// Data Models +import 'view_type.dart'; + +/// The properties of the [PageView] widget in the [ViewType.page] view. +class PageOptions { + /// Creates a object that contains the properties of the [PageView] widget. + const PageOptions({ + this.clipBehavior = Clip.hardEdge, + this.pageSnapping = true, + this.padEnds = true, + this.scrollBehavior, + this.allowImplicitScrolling = false, + this.dragStartBehavior = DragStartBehavior.start, + }); + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// Set to false to disable page snapping, useful for custom scroll behavior. + /// + /// If the [padEnds] is false and [PageController.viewportFraction] < 1.0, + /// the page will snap to the beginning of the viewport; otherwise, the page + /// will snap to the center of the viewport. + final bool pageSnapping; + + /// Whether to add padding to both ends of the list. + /// + /// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added + /// such that the first and last child slivers will be in the center of + /// the viewport when scrolled all the way to the start or end, respectively. + /// + /// If [PageController.viewportFraction] >= 1.0, this property has no effect. + /// + /// This property defaults to true. + final bool padEnds; + + /// {@macro flutter.widgets.shadow.scrollBehavior} + /// + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + /// + /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be + /// modified by default to not apply a [Scrollbar]. + final ScrollBehavior? scrollBehavior; + + /// Controls whether the widget's pages will respond to + /// [RenderObject.showOnScreen], which will allow for implicit accessibility + /// scrolling. + /// + /// With this flag set to false, when accessibility focus reaches the end of + /// the current page and the user attempts to move it to the next element, the + /// focus will traverse to the next widget outside of the page view. + /// + /// With this flag set to true, when accessibility focus reaches the end of + /// the current page and user attempts to move it to the next element, focus + /// will traverse to the next page in the page view. + final bool allowImplicitScrolling; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; +} diff --git a/lib/widget/firebase_pagination/src/models/view_type.dart b/lib/widget/firebase_pagination/src/models/view_type.dart new file mode 100644 index 0000000..f25eb1c --- /dev/null +++ b/lib/widget/firebase_pagination/src/models/view_type.dart @@ -0,0 +1,19 @@ +// Flutter Packages +import 'package:flutter/widgets.dart'; + +/// The [ScrollView] to use for the loaded data. +/// +/// Supports [list], [grid], and [wrap]. +enum ViewType { + /// Loads the data as a [ListView]. + list, + + /// Loads the data as a [GridView]. + grid, + + /// Loads the data as a scrollable [Wrap]. + wrap, + + /// Loads the data as a [PageView]. + page, +} diff --git a/lib/widget/firebase_pagination/src/models/wrap_options.dart b/lib/widget/firebase_pagination/src/models/wrap_options.dart new file mode 100644 index 0000000..5afac7b --- /dev/null +++ b/lib/widget/firebase_pagination/src/models/wrap_options.dart @@ -0,0 +1,67 @@ +// Flutter Packages +import 'package:flutter/widgets.dart'; + +// Data Models +import 'view_type.dart'; + +/// The properties of the [Wrap] widget in the [ViewType.wrap] view. +class WrapOptions { + /// Creates a object that contains the properties of the [Wrap] widget. + const WrapOptions({ + this.direction = Axis.horizontal, + this.alignment = WrapAlignment.center, + this.spacing = 5.0, + this.runAlignment = WrapAlignment.start, + this.runSpacing = 5.0, + this.crossAxisAlignment = WrapCrossAlignment.start, + this.textDirection, + this.verticalDirection = VerticalDirection.down, + this.clipBehavior = Clip.none, + }); + + /// The direction to use as the main axis. + /// + /// Defaults to [Axis.horizontal]. + final Axis direction; + + /// How the children within a run should be placed in the main axis. + /// + /// Defaults to [WrapAlignment.center]. + final WrapAlignment alignment; + + /// How much space to place between children in a run in the main axis. + /// + /// Defaults to 5.0. + final double spacing; + + /// How the runs themselves should be placed in the cross axis. + /// + /// Defaults to [WrapAlignment.start]. + final WrapAlignment runAlignment; + + /// How much space to place between the runs themselves in the cross axis. + /// + /// Defaults to 5.0. + final double runSpacing; + + /// How the children within a run should be aligned relative to each other in + /// the cross axis. + /// + /// Defaults to [WrapCrossAlignment.start]. + final WrapCrossAlignment crossAxisAlignment; + + /// Determines the order to lay children out horizontally and how to interpret + /// `start` and `end` in the horizontal direction. + final TextDirection? textDirection; + + /// Determines the order to lay children out vertically and how to interpret + /// `start` and `end` in the vertical direction. + /// + /// Defaults to [VerticalDirection.down]. + final VerticalDirection verticalDirection; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; +} diff --git a/lib/widget/firebase_pagination/src/realtime_db_pagination.dart b/lib/widget/firebase_pagination/src/realtime_db_pagination.dart new file mode 100644 index 0000000..6cbdb02 --- /dev/null +++ b/lib/widget/firebase_pagination/src/realtime_db_pagination.dart @@ -0,0 +1,400 @@ +// Dart Packages +import 'dart:async'; + +// Firebase Packages +import 'package:firebase_database/firebase_database.dart'; +// Flutter Packages +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +// Functions +import 'functions/separator_builder.dart'; +// Data Models +import 'models/page_options.dart'; +import 'models/view_type.dart'; +import 'models/wrap_options.dart'; +// Widgets +import 'widgets/defaults/bottom_loader.dart'; +import 'widgets/defaults/empty_screen.dart'; +import 'widgets/defaults/initial_loader.dart'; +import 'widgets/views/build_pagination.dart'; + +/// A [StreamBuilder] that automatically loads more data when the user scrolls +/// to the bottom. +/// +/// Optimized for [FirebaseDatabase] with fields like `createdAt` and +/// `timestamp` to sort the data. +/// +/// Supports live updates and realtime updates to loaded data. +/// +/// Data can be represented in a [ListView], [GridView] or scollable [Wrap]. +class RealtimeDBPagination extends StatefulWidget { + /// Creates a [StreamBuilder] widget that automatically loads more data when + /// the user scrolls to the bottom. + /// + /// Optimized for [FirebaseDatabase] with fields like `createdAt` and + /// `timestamp` to sort the data. + /// + /// Supports live updates and realtime updates to loaded data. + /// + /// Data can be represented in a [ListView], [GridView] or scollable [Wrap]. + const RealtimeDBPagination({ + required this.query, + required this.itemBuilder, + required this.orderBy, + super.key, + this.descending = false, + this.separatorBuilder, + this.limit = 10, + this.viewType = ViewType.list, + this.isLive = false, + this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + this.wrapOptions = const WrapOptions(), + this.pageOptions = const PageOptions(), + this.onEmpty = const EmptyScreen(), + this.bottomLoader = const BottomLoader(), + this.initialLoader = const InitialLoader(), + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.shrinkWrap = false, + this.physics, + this.padding, + this.controller, + this.pageController, + }); + + /// The query to use to fetch data from Firebase Realtime Database. + /// + /// ### Note: + /// - The query must **NOT** contain a `limitToFirst` or `limitToLast` itself. + /// - The `limit` must be set using the [limit] property of this widget. + final Query query; + + /// The builder to use to build the items in the list. + /// + /// The builder is passed the build context, snapshot of data and index of + /// the item in the list. + final Widget Function(BuildContext, List, int) itemBuilder; + + /// The field to use to sort the data. Give the same value as the field + /// used to order the data in the query. + /// + /// ## Example + /// If the query is: + /// ```dart + /// FirebaseDatabase.instance.ref('messages').orderByChild('createdAt') + /// ``` + /// Then the value of [orderBy] should be `createdAt`. + /// + /// If null, the data will be sorted by the key. + final String? orderBy; + + /// Fetches data is decending order for the given [orderBy] field. + /// + /// Default value is `false`. + final bool descending; + + /// The builder to use to render the separator. + /// + /// Only used if [viewType] is [ViewType.list]. + /// + /// Default [Widget] is [SizedBox.shrink]. + final Widget Function(BuildContext, int)? separatorBuilder; + + /// The number of items to fetch from Firebase Realtime Database at once. + /// + /// Defaults to `10`. + final int limit; + + /// The type of view to use for the list. + /// + /// Defaults to [ViewType.list]. + final ViewType viewType; + + /// Whether to fetch newly added items as they are added to + /// Firebase Realtime Database. + /// + /// Defaults to `false`. + final bool isLive; + + /// The delegate to use for the [GridView]. + /// + /// Defaults to [SliverGridDelegateWithFixedCrossAxisCount]. + final SliverGridDelegate gridDelegate; + + /// The [Wrap] widget properties to use. + /// + /// Defaults to [WrapOptions]. + final WrapOptions wrapOptions; + + /// The [PageView] properties to use. + /// + /// Defaults to [PageOptions]. + final PageOptions pageOptions; + + /// The widget to use when data is empty. + /// + /// Defaults to [EmptyScreen]. + final Widget onEmpty; + + /// The widget to use when more data is loading. + /// + /// Defaults to [BottomLoader]. + final Widget bottomLoader; + + /// The widget to use when data is loading initially. + /// + /// Defaults to [InitialLoader]. + final Widget initialLoader; + + /// The scrolling direction of the [ScrollView]. + final Axis scrollDirection; + + /// Whether the [ScrollView] scrolls in the reading direction. + final bool reverse; + + /// Should the [ScrollView] be shrink-wrapped. + final bool shrinkWrap; + + /// The scroll behavior to use for the [ScrollView]. + final ScrollPhysics? physics; + + /// The padding to use for the [ScrollView]. + final EdgeInsetsGeometry? padding; + + /// The scroll controller to use for the [ScrollView]. + /// + /// Defaults to [ScrollController]. + final ScrollController? controller; + + /// The page controller to use for the [PageView]. + /// + /// Defaults to [PageController]. + final PageController? pageController; + + @override + State createState() => _RealtimeDBPaginationState(); +} + +/// The state of the [RealtimeDBPagination] widget. +class _RealtimeDBPaginationState extends State { + /// All the data that has been loaded from Firebase Realtime Database. + final List _data = []; + + /// Snapshot subscription for the query. + /// + /// Also handles updates to loaded data. + StreamSubscription? _streamSub; + + /// Snapshot subscription for the query to handle newly added data. + StreamSubscription? _liveStreamSub; + + /// [ScrollController] to listen to scroll end and load more data. + late final ScrollController _controller = widget.controller ?? ScrollController(); + + /// [PageController] to listen to page changes and load more data. + late final PageController _pageController = widget.pageController ?? PageController(); + + /// Whether initial data is loading. + bool _isInitialLoading = true; + + /// Whether more data is loading. + bool _isFetching = false; + + /// Whether the end for given query has been reached. + /// + /// This is used to determine if more data should be loaded when the user + /// scrolls to the bottom. + bool _isEnded = false; + + /// Loads more data from Firebase Realtime Database and handles + /// updates to loaded data. + /// + /// Setting [getMore] to `false` will only set listener for the + /// currently loaded data. + Future _loadData({bool getMore = true}) async { + // To cancel previous updates listener when new one is set. + final tempSub = _streamSub; + + if (getMore) setState(() => _isFetching = true); + + // Sets limit of nodes to fetch. + // If currently 15 items are loaded, and limit is 5 then total 20 items + // will be fetched including the ones already present. + final docsLimit = _data.length + (getMore ? widget.limit : 0); + var docsQuery = widget.descending ? widget.query.limitToLast(docsLimit) : widget.query.limitToFirst(docsLimit); + + if (_data.isNotEmpty) { + if (widget.descending) { + // Sets ending point from where before data should be fetched. + // If currently 15 items are loaded, and limit is 5 then total 20 items + // will be fetched where below mentioned value will be the largest and + // last in the fetched array (But first in callback array as using + // reversed in build method) + docsQuery = docsQuery.endAt( + Map.from( + _data.last.value! as Map, + )[widget.orderBy], + ); + } else { + // Sets starting point from where after data should be fetched. + // If currently 15 items are loaded, and limit is 5 then total 20 items + // will be fetched where below mentioned value will be the smallest and + // first in array + docsQuery = docsQuery.startAt( + Map.from( + _data.first.value! as Map, + )[widget.orderBy], + ); + } + } + + _streamSub = docsQuery.onValue.listen((DatabaseEvent snapshot) async { + await tempSub?.cancel(); + + _data + ..clear() + ..addAll(snapshot.snapshot.children); + + // To set new updates listener for the existing data + // or to set new live listener if the first data node is removed. + final isDataRemoved = snapshot.type == DatabaseEventType.childRemoved; + + _isFetching = false; + if (!isDataRemoved) { + _isEnded = snapshot.snapshot.children.length < docsLimit; + } + + if (isDataRemoved || _isInitialLoading) { + _isInitialLoading = false; + if (snapshot.snapshot.children.isNotEmpty) { + // Set updates listener for the existing data starting from the + // first data node only. + await _loadData(getMore: false); + } else { + _streamSub?.cancel(); + } + if (widget.isLive) _setLiveListener(); + } + + if (mounted) setState(() {}); + + // Add data till the view is scrollable. This ensures that the user can + // scroll to the bottom and load more data. + if (_isInitialLoading || _isFetching || _isEnded) return; + SchedulerBinding.instance.addPostFrameCallback((_) { + if (_controller.hasClients && _controller.position.maxScrollExtent <= 0) { + _loadData(); + } + }); + }); + } + + /// Sets the live listener for the query. + /// + /// Fires when new data is added to the query. + Future _setLiveListener() async { + // To cancel previous live listener when new one is set. + final tempSub = _liveStreamSub; + + var latestDocQuery = widget.descending ? widget.query.limitToLast(1) : widget.query.limitToFirst(1); + + if (_data.isNotEmpty) { + if (widget.descending) { + // Sets query to fetch data after the last element in the array, + // which is the largest value. + latestDocQuery = latestDocQuery.startAfter( + Map.from( + _data.last.value! as Map, + )[widget.orderBy], + ); + } else { + // Sets query to fetch data before the first element in the array, + // whch is the smallest value + latestDocQuery = latestDocQuery.endBefore( + Map.from( + _data.first.value! as Map, + )[widget.orderBy], + ); + } + } + + _liveStreamSub = latestDocQuery.onValue.listen( + (DatabaseEvent snapshot) async { + await tempSub?.cancel(); + if (snapshot.snapshot.children.isEmpty) return; + + _data.insert( + widget.descending ? _data.length : 0, + snapshot.snapshot.children.first, + ); + + // To handle newly added data after this curently loaded data. + await _setLiveListener(); + + // Set updates listener for the newly added data. + _loadData(getMore: false); + }, + ); + } + + /// To handle scroll end event and load more data. + void _scrollListener() { + if (_isInitialLoading || _isFetching || _isEnded) return; + if (!_controller.hasClients) return; + + final position = _controller.position; + if (position.pixels >= (position.maxScrollExtent - 50)) { + _loadData(); + } + } + + @override + void initState() { + super.initState(); + _loadData(); + _controller.addListener(_scrollListener); + } + + @override + void dispose() { + _streamSub?.cancel(); + _liveStreamSub?.cancel(); + _controller + ..removeListener(_scrollListener) + ..dispose(); + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _isInitialLoading + ? widget.initialLoader + : _data.isEmpty + ? widget.onEmpty + : BuildPagination( + items: widget.descending ? _data.reversed.toList() : _data, + itemBuilder: widget.itemBuilder, + separatorBuilder: widget.separatorBuilder ?? separatorBuilder, + isLoading: _isFetching, + viewType: widget.viewType, + bottomLoader: widget.bottomLoader, + gridDelegate: widget.gridDelegate, + wrapOptions: widget.wrapOptions, + pageOptions: widget.pageOptions, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: _controller, + pageController: _pageController, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + padding: widget.padding, + onPageChanged: (index) { + if (index >= _data.length - 1) _loadData(); + }, + ); + } +} diff --git a/lib/widget/firebase_pagination/src/widgets/defaults/bottom_loader.dart b/lib/widget/firebase_pagination/src/widgets/defaults/bottom_loader.dart new file mode 100644 index 0000000..fd59c2f --- /dev/null +++ b/lib/widget/firebase_pagination/src/widgets/defaults/bottom_loader.dart @@ -0,0 +1,28 @@ +// Flutter Packages +import 'package:flutter/material.dart'; + +/// A circular progress indicator that spins when the [Stream] is loading. +/// +/// Used at the bottom of a [ScrollView] to indicate that more data is loading. +class BottomLoader extends StatelessWidget { + /// Creates a circular progress indicator that spins when the [Stream] is + /// loading. + /// + /// Used at the bottom of a [ScrollView] to indicate that more data is + /// loading. + const BottomLoader({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 25, + height: 25, + margin: const EdgeInsets.all(10), + child: const CircularProgressIndicator.adaptive( + strokeWidth: 2.5, + ), + ), + ); + } +} diff --git a/lib/widget/firebase_pagination/src/widgets/defaults/empty_screen.dart b/lib/widget/firebase_pagination/src/widgets/defaults/empty_screen.dart new file mode 100644 index 0000000..7c9c853 --- /dev/null +++ b/lib/widget/firebase_pagination/src/widgets/defaults/empty_screen.dart @@ -0,0 +1,16 @@ +// Flutter Packages +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +/// A [Widget] to show when there is no data to display. +class EmptyScreen extends StatelessWidget { + /// Creates a [Widget] to show when there is no data to display. + const EmptyScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text('Nothing found here...'.tr), + ); + } +} diff --git a/lib/widget/firebase_pagination/src/widgets/defaults/initial_loader.dart b/lib/widget/firebase_pagination/src/widgets/defaults/initial_loader.dart new file mode 100644 index 0000000..20f3111 --- /dev/null +++ b/lib/widget/firebase_pagination/src/widgets/defaults/initial_loader.dart @@ -0,0 +1,20 @@ +// Flutter Packages +import 'package:flutter/material.dart'; + +/// A circular progress indicator that spins when the [Stream] is loading. +/// +/// Used when the [Stream] is loading the first time. +class InitialLoader extends StatelessWidget { + /// Creates a circular progress indicator that spins when the [Stream] is + /// loading. + /// + /// Used when the [Stream] is loading the first time. + const InitialLoader({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } +} diff --git a/lib/widget/firebase_pagination/src/widgets/views/build_pagination.dart b/lib/widget/firebase_pagination/src/widgets/views/build_pagination.dart new file mode 100644 index 0000000..2e8ef98 --- /dev/null +++ b/lib/widget/firebase_pagination/src/widgets/views/build_pagination.dart @@ -0,0 +1,177 @@ +// Flutter Packages +import 'package:customer/widget/firebase_pagination/src/models/page_options.dart'; +import 'package:customer/widget/firebase_pagination/src/models/view_type.dart'; +import 'package:customer/widget/firebase_pagination/src/models/wrap_options.dart'; +import 'package:flutter/material.dart'; + +/// A [ScrollView] to use for the provided [items]. +/// +/// The [items] are loaded into the [ScrollView] based on the [viewType]. +class BuildPagination extends StatelessWidget { + /// Creates a [ScrollView] to use for the provided [items]. + /// + /// The [items] are rendered in the [ScrollView] using the [itemBuilder]. + /// + /// The [viewType] determines the type of [ScrollView] to use. + const BuildPagination({ + super.key, + required this.items, + required this.itemBuilder, + required this.separatorBuilder, + required this.isLoading, + required this.viewType, + required this.bottomLoader, + required this.gridDelegate, + required this.wrapOptions, + required this.pageOptions, + required this.scrollDirection, + required this.reverse, + required this.controller, + required this.pageController, + required this.shrinkWrap, + this.physics, + this.padding, + this.onPageChanged, + }); + + /// The items to display in the [ScrollView]. + final List items; + + /// The builder to use to render the items. + final Widget Function(BuildContext, List, int) itemBuilder; + + /// The builder to use to render the separator. + /// + /// Only used if [viewType] is [ViewType.list]. + final Widget Function(BuildContext, int) separatorBuilder; + + /// Whether more [items] are being loaded. + final bool isLoading; + + /// The type of [ScrollView] to use. + final ViewType viewType; + + /// A [Widget] to show when more [items] are being loaded. + final Widget bottomLoader; + + /// The delegate to use for the [GridView]. + final SliverGridDelegate gridDelegate; + + /// The options to use for the [Wrap]. + final WrapOptions wrapOptions; + + /// The options to use for the [PageView]. + final PageOptions pageOptions; + + /// The scrolling direction of the [ScrollView]. + final Axis scrollDirection; + + /// Whether the [ScrollView] scrolls in the reading direction. + final bool reverse; + + /// The scroll controller to handle the scroll events. + final ScrollController controller; + + /// The page controller to handle page view events. + final PageController pageController; + + /// Should the [ScrollView] be shrink-wrapped. + final bool shrinkWrap; + + /// The scroll behavior to use for the [ScrollView]. + final ScrollPhysics? physics; + + /// The padding to use for the [ScrollView]. + final EdgeInsetsGeometry? padding; + + /// Specifies what to do when page changes in the [PageView]. + final void Function(int)? onPageChanged; + + @override + Widget build(BuildContext context) { + switch (viewType) { + case ViewType.list: + return ListView.separated( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + itemCount: items.length + (isLoading ? 1 : 0), + itemBuilder: (BuildContext context, int index) { + if (index >= items.length) return bottomLoader; + + return itemBuilder(context, items, index); + }, + separatorBuilder: separatorBuilder, + ); + + case ViewType.grid: + return GridView.builder( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + itemCount: items.length + (isLoading ? 1 : 0), + itemBuilder: (BuildContext context, int index) { + if (index >= items.length) return bottomLoader; + + return itemBuilder(context, items, index); + }, + gridDelegate: gridDelegate, + ); + + case ViewType.wrap: + return SingleChildScrollView( + scrollDirection: scrollDirection, + reverse: reverse, + padding: padding, + physics: physics, + controller: controller, + child: Wrap( + direction: wrapOptions.direction, + alignment: wrapOptions.alignment, + spacing: wrapOptions.spacing, + runAlignment: wrapOptions.runAlignment, + runSpacing: wrapOptions.runSpacing, + crossAxisAlignment: wrapOptions.crossAxisAlignment, + textDirection: wrapOptions.textDirection, + verticalDirection: wrapOptions.verticalDirection, + clipBehavior: wrapOptions.clipBehavior, + children: List.generate( + items.length + (isLoading ? 1 : 0), + (int index) { + if (index >= items.length) return bottomLoader; + + return itemBuilder(context, items, index); + }, + ), + ), + ); + + case ViewType.page: + return PageView.builder( + scrollDirection: scrollDirection, + reverse: reverse, + controller: pageController, + physics: physics, + clipBehavior: pageOptions.clipBehavior, + pageSnapping: pageOptions.pageSnapping, + onPageChanged: onPageChanged, + padEnds: pageOptions.padEnds, + scrollBehavior: pageOptions.scrollBehavior, + allowImplicitScrolling: pageOptions.allowImplicitScrolling, + dragStartBehavior: pageOptions.dragStartBehavior, + itemCount: items.length + (isLoading ? 1 : 0), + itemBuilder: (BuildContext context, int index) { + if (index >= items.length) return bottomLoader; + + return itemBuilder(context, items, index); + }, + ); + } + } +} diff --git a/lib/widget/geoflutterfire/geoflutterfire.dart b/lib/widget/geoflutterfire/geoflutterfire.dart new file mode 100644 index 0000000..0bedbb1 --- /dev/null +++ b/lib/widget/geoflutterfire/geoflutterfire.dart @@ -0,0 +1,6 @@ +library; + +export 'src/collection/default.dart'; +export 'src/geoflutterfire.dart'; +export 'src/models/distance_doc_snapshot.dart'; +export 'src/models/point.dart'; diff --git a/lib/widget/geoflutterfire/src/collection/base.dart b/lib/widget/geoflutterfire/src/collection/base.dart new file mode 100644 index 0000000..7933462 --- /dev/null +++ b/lib/widget/geoflutterfire/src/collection/base.dart @@ -0,0 +1,189 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../models/distance_doc_snapshot.dart'; +import '../models/point.dart'; +import '../utils/arrays.dart'; +import '../utils/math.dart'; + +class BaseGeoFireCollectionRef { + final Query _collectionReference; + late final Stream>? _stream; + + BaseGeoFireCollectionRef(this._collectionReference) { + _stream = _createStream(_collectionReference).shareReplay(maxSize: 1); + } + + /// return QuerySnapshot stream + Stream>? snapshot() { + return _stream; + } + + /// return the Document mapped to the [id] + Stream>> data(String id) { + return _stream!.map((querySnapshot) { + querySnapshot.docs.where((documentSnapshot) { + return documentSnapshot.id == id; + }); + return querySnapshot.docs; + }); + } + + /// add a document to collection with [data] + Future> add( + T data, + ) { + try { + final colRef = _collectionReference as CollectionReference; + return colRef.add(data); + } catch (e) { + throw Exception('cannot call add on Query, use collection reference instead'); + } + } + + /// delete document with [id] from the collection + Future delete(id) { + try { + CollectionReference colRef = _collectionReference as CollectionReference; + return colRef.doc(id).delete(); + } catch (e) { + throw Exception('cannot call delete on Query, use collection reference instead'); + } + } + + /// create or update a document with [id], [merge] defines whether the document should overwrite + Future setDoc(String id, Object? data, {bool merge = false}) { + try { + CollectionReference colRef = _collectionReference as CollectionReference; + return colRef.doc(id).set(data, SetOptions(merge: merge)); + } catch (e) { + throw Exception('cannot call set on Query, use collection reference instead'); + } + } + + /// set a geo point with [latitude] and [longitude] using [field] as the object key to the document with [id] + Future setPoint( + String id, + String field, + double latitude, + double longitude, + ) { + try { + CollectionReference colRef = _collectionReference as CollectionReference; + var point = GeoFirePoint(latitude, longitude).data; + return colRef.doc(id).set({field: point}, SetOptions(merge: true)); + } catch (e) { + throw Exception('cannot call set on Query, use collection reference instead'); + } + } + + @protected + Stream>> protectedWithin({ + required GeoFirePoint center, + required double radius, + required String field, + required GeoPoint? Function(T t) geopointFrom, + required bool? strictMode, + }) => + protectedWithinWithDistance( + center: center, + radius: radius, + field: field, + geopointFrom: geopointFrom, + strictMode: strictMode, + ).map((snapshots) => snapshots.map((snapshot) => snapshot.documentSnapshot).toList()); + + /// query firestore documents based on geographic [radius] from geoFirePoint [center] + /// [field] specifies the name of the key in the document + @protected + Stream>> protectedWithinWithDistance({ + required GeoFirePoint center, + required double radius, + required String field, + required GeoPoint? Function(T t) geopointFrom, + required bool? strictMode, + }) { + final nonNullStrictMode = strictMode ?? false; + + final precision = MathUtils.setPrecision(radius); + final centerHash = center.hash.substring(0, precision); + final area = GeoFirePoint.neighborsOf(hash: centerHash)..add(centerHash); + + final queries = area.map((hash) { + final tempQuery = _queryPoint(hash, field); + return _createStream(tempQuery).map((querySnapshot) { + return querySnapshot.docs; + }); + }); + + final mergedObservable = mergeObservable(queries); + + final filtered = mergedObservable.map((list) { + final mappedList = list.map((documentSnapshot) { + final snapData = documentSnapshot.exists ? documentSnapshot.data() : null; + + assert(snapData != null, 'Data in one of the docs is empty'); + if (snapData == null) return null; + // We will handle it to fail gracefully + + final geoPoint = geopointFrom(snapData); + assert(geoPoint != null, 'Couldnt find geopoint from stored data'); + if (geoPoint == null) return null; + // We will handle it to fail gracefully + + final kmDistance = center.kmDistance( + lat: geoPoint.latitude, + lng: geoPoint.longitude, + ); + return DistanceDocSnapshot( + documentSnapshot: documentSnapshot, + kmDistance: kmDistance, + ); + }); + + final nullableFilteredList = nonNullStrictMode + ? mappedList + .where((doc) => doc != null && doc.kmDistance <= radius * 1.02 // buffer for edge distances; + ) + .toList() + : mappedList.toList(); + final filteredList = nullableFilteredList.whereNotNull().toList(); + + filteredList.sort( + (a, b) => (a.kmDistance * 1000).toInt() - (b.kmDistance * 1000).toInt(), + ); + return filteredList; + }); + return filtered.asBroadcastStream(); + } + + Stream>> mergeObservable( + Iterable>>> queries, + ) { + final mergedObservable = Rx.combineLatest>, List>>(queries, (originalList) { + final reducedList = >[]; + for (final t in originalList) { + reducedList.addAll(t); + } + return reducedList; + }); + return mergedObservable; + } + + /// INTERNAL FUNCTIONS + + /// construct a query for the [geoHash] and [field] + Query _queryPoint(String geoHash, String field) { + final end = '$geoHash~'; + final temp = _collectionReference; + return temp.orderBy('$field.geohash').startAt([geoHash]).endAt([end]); + } + + /// create an observable for [ref], [ref] can be [Query] or [CollectionReference] + Stream> _createStream(Query ref) { + return ref.snapshots(); + } +} diff --git a/lib/widget/geoflutterfire/src/collection/default.dart b/lib/widget/geoflutterfire/src/collection/default.dart new file mode 100644 index 0000000..044dfb4 --- /dev/null +++ b/lib/widget/geoflutterfire/src/collection/default.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/widget/geoflutterfire/src/models/distance_doc_snapshot.dart'; +import 'package:customer/widget/geoflutterfire/src/models/point.dart'; +import 'package:flutter/cupertino.dart'; + +import 'base.dart'; + +class GeoFireCollectionRef extends BaseGeoFireCollectionRef> { + GeoFireCollectionRef(super.collectionReference); + + Stream>>> within({ + required GeoFirePoint center, + required double radius, + required String field, + bool? strictMode, + }) { + return protectedWithin( + center: center, + radius: radius, + field: field, + geopointFrom: (snapData) => geopointFromMap( + field: field, + snapData: snapData, + ), + strictMode: strictMode, + ); + } + + Stream>>> withinWithDistance({ + required GeoFirePoint center, + required double radius, + required String field, + bool? strictMode, + }) { + return protectedWithinWithDistance( + center: center, + radius: radius, + field: field, + geopointFrom: (snapData) => geopointFromMap( + field: field, + snapData: snapData, + ), + strictMode: strictMode, + ); + } + + @visibleForTesting + static GeoPoint? geopointFromMap({ + required String field, + required Map snapData, + }) { + // split and fetch geoPoint from the nested Map + final fieldList = field.split('.'); + Map? geoPointField = snapData[fieldList[0]]; + if (fieldList.length > 1) { + for (int i = 1; i < fieldList.length; i++) { + geoPointField = geoPointField?[fieldList[i]]; + } + } + return geoPointField?['geopoint'] as GeoPoint?; + } +} diff --git a/lib/widget/geoflutterfire/src/collection/with_converter.dart b/lib/widget/geoflutterfire/src/collection/with_converter.dart new file mode 100644 index 0000000..2922237 --- /dev/null +++ b/lib/widget/geoflutterfire/src/collection/with_converter.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/widget/geoflutterfire/src/models/distance_doc_snapshot.dart'; +import 'package:customer/widget/geoflutterfire/src/models/point.dart'; + +import 'base.dart'; + +class GeoFireCollectionWithConverterRef extends BaseGeoFireCollectionRef { + GeoFireCollectionWithConverterRef(super.collectionReference); + + Stream>> within({ + required GeoFirePoint center, + required double radius, + required String field, + required GeoPoint Function(T) geopointFrom, + bool? strictMode, + }) { + return protectedWithin( + center: center, + radius: radius, + field: field, + geopointFrom: geopointFrom, + strictMode: strictMode, + ); + } + + Stream>> withinWithDistance({ + required GeoFirePoint center, + required double radius, + required String field, + required GeoPoint Function(T) geopointFrom, + bool? strictMode, + }) { + return protectedWithinWithDistance( + center: center, + radius: radius, + field: field, + geopointFrom: geopointFrom, + strictMode: strictMode, + ); + } +} diff --git a/lib/widget/geoflutterfire/src/geoflutterfire.dart b/lib/widget/geoflutterfire/src/geoflutterfire.dart new file mode 100644 index 0000000..f7aa1b5 --- /dev/null +++ b/lib/widget/geoflutterfire/src/geoflutterfire.dart @@ -0,0 +1,31 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:customer/widget/geoflutterfire/src/collection/with_converter.dart'; + +import 'collection/default.dart'; +import 'models/point.dart'; + +class Geoflutterfire { + Geoflutterfire(); + + GeoFireCollectionRef collection({ + required Query> collectionRef, + }) { + return GeoFireCollectionRef(collectionRef); + } + + GeoFireCollectionWithConverterRef collectionWithConverter({ + required Query collectionRef, + }) { + return GeoFireCollectionWithConverterRef(collectionRef); + } + + GeoFireCollectionRef customCollection({ + required Query> collectionRef, + }) { + return GeoFireCollectionRef(collectionRef); + } + + GeoFirePoint point({required double latitude, required double longitude}) { + return GeoFirePoint(latitude, longitude); + } +} diff --git a/lib/widget/geoflutterfire/src/models/distance_doc_snapshot.dart b/lib/widget/geoflutterfire/src/models/distance_doc_snapshot.dart new file mode 100644 index 0000000..2be76da --- /dev/null +++ b/lib/widget/geoflutterfire/src/models/distance_doc_snapshot.dart @@ -0,0 +1,11 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class DistanceDocSnapshot { + final DocumentSnapshot documentSnapshot; + final double kmDistance; + + DistanceDocSnapshot({ + required this.documentSnapshot, + required this.kmDistance, + }); +} diff --git a/lib/widget/geoflutterfire/src/models/point.dart b/lib/widget/geoflutterfire/src/models/point.dart new file mode 100644 index 0000000..3bc112c --- /dev/null +++ b/lib/widget/geoflutterfire/src/models/point.dart @@ -0,0 +1,60 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +import '../utils/math.dart'; + +class GeoFirePoint { + static final MathUtils _util = MathUtils(); + double latitude, longitude; + + GeoFirePoint(this.latitude, this.longitude); + + /// return geographical distance between two Co-ordinates + static double kmDistanceBetween({required Coordinates to, required Coordinates from}) { + return MathUtils.kmDistance(to, from); + } + + /// return neighboring geo-hashes of [hash] + static List neighborsOf({required String hash}) { + return _util.neighbors(hash); + } + + /// return hash of [GeoFirePoint] + String get hash { + return _util.encode(latitude, longitude, 9); + } + + /// return all neighbors of [GeoFirePoint] + List get neighbors { + return _util.neighbors(hash); + } + + /// return [GeoPoint] of [GeoFirePoint] + GeoPoint get geoPoint { + return GeoPoint(latitude, longitude); + } + + Coordinates get coords { + return Coordinates(latitude, longitude); + } + + /// return distance between [GeoFirePoint] and ([lat], [lng]) + double kmDistance({required double lat, required double lng}) { + return kmDistanceBetween(from: coords, to: Coordinates(lat, lng)); + } + + Map get data { + return {'geopoint': geoPoint, 'geohash': hash}; + } + + /// haversine distance between [GeoFirePoint] and ([lat], [lng]) + double haversineDistance({required double lat, required double lng}) { + return GeoFirePoint.kmDistanceBetween(from: coords, to: Coordinates(lat, lng)); + } +} + +class Coordinates { + double latitude; + double longitude; + + Coordinates(this.latitude, this.longitude); +} diff --git a/lib/widget/geoflutterfire/src/utils/arrays.dart b/lib/widget/geoflutterfire/src/utils/arrays.dart new file mode 100644 index 0000000..d8374bb --- /dev/null +++ b/lib/widget/geoflutterfire/src/utils/arrays.dart @@ -0,0 +1,5 @@ +extension NullableListExtensions on Iterable { + Iterable whereNotNull() { + return where((e) => e != null).map((e) => e as T); + } +} diff --git a/lib/widget/geoflutterfire/src/utils/math.dart b/lib/widget/geoflutterfire/src/utils/math.dart new file mode 100644 index 0000000..5edd607 --- /dev/null +++ b/lib/widget/geoflutterfire/src/utils/math.dart @@ -0,0 +1,271 @@ +import 'dart:math'; + +import '../models/point.dart'; + +class MathUtils { + static const base32Codes = '0123456789bcdefghjkmnpqrstuvwxyz'; + Map base32CodesDic = {}; + + MathUtils() { + for (var i = 0; i < base32Codes.length; i++) { + base32CodesDic.putIfAbsent(base32Codes[i], () => i); + } + } + + var encodeAuto = 'auto'; + + /// + /// Significant Figure Hash Length + /// + /// This is a quick and dirty lookup to figure out how long our hash + /// should be in order to guarantee a certain amount of trailing + /// significant figures. This was calculated by determining the error: + /// 45/2^(n-1) where n is the number of bits for a latitude or + /// longitude. Key is # of desired sig figs, value is minimum length of + /// the geohash. + /// @type Array + // Desired sig figs: 0 1 2 3 4 5 6 7 8 9 10 + var sigfigHashLength = [0, 5, 7, 8, 11, 12, 13, 15, 16, 17, 18]; + + /// + /// Encode + /// Create a geohash from latitude and longitude + /// that is 'number of chars' long + String encode(var latitude, var longitude, var numberOfChars) { + if (numberOfChars == encodeAuto) { + if (latitude.runtimeType == double || longitude.runtimeType == double) { + throw Exception('string notation required for auto precision.'); + } + int decSigFigsLat = latitude.split('.')[1].length; + int decSigFigsLon = longitude.split('.')[1].length; + int numberOfSigFigs = max(decSigFigsLat, decSigFigsLon); + numberOfChars = sigfigHashLength[numberOfSigFigs]; + } else { + numberOfChars ??= 9; + } + + var chars = [], bits = 0, bitsTotal = 0, hashValue = 0; + double maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid; + + while (chars.length < numberOfChars) { + if (bitsTotal % 2 == 0) { + mid = (maxLon + minLon) / 2; + if (longitude > mid) { + hashValue = (hashValue << 1) + 1; + minLon = mid; + } else { + hashValue = (hashValue << 1) + 0; + maxLon = mid; + } + } else { + mid = (maxLat + minLat) / 2; + if (latitude > mid) { + hashValue = (hashValue << 1) + 1; + minLat = mid; + } else { + hashValue = (hashValue << 1) + 0; + maxLat = mid; + } + } + + bits++; + bitsTotal++; + if (bits == 5) { + var code = base32Codes[hashValue]; + chars.add(code); + bits = 0; + hashValue = 0; + } + } + + return chars.join(''); + } + + /// + /// Decode Bounding box + /// + /// Decode a hashString into a bound box that matches it. + /// Data returned in a List [minLat, minLon, maxLat, maxLon] + List decodeBbox(String hashString) { + var isLon = true; + double maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid; + + int? hashValue = 0; + for (var i = 0, l = hashString.length; i < l; i++) { + var code = hashString[i].toLowerCase(); + hashValue = base32CodesDic[code]; + + for (var bits = 4; bits >= 0; bits--) { + var bit = (hashValue! >> bits) & 1; + if (isLon) { + mid = (maxLon + minLon) / 2; + if (bit == 1) { + minLon = mid; + } else { + maxLon = mid; + } + } else { + mid = (maxLat + minLat) / 2; + if (bit == 1) { + minLat = mid; + } else { + maxLat = mid; + } + } + isLon = !isLon; + } + } + return [minLat, minLon, maxLat, maxLon]; + } + + /// + /// Decode a [hashString] into a pair of latitude and longitude. + /// A map is returned with keys 'latitude', 'longitude','latitudeError','longitudeError' + Map decode(String hashString) { + List bbox = decodeBbox(hashString); + double lat = (bbox[0] + bbox[2]) / 2; + double lon = (bbox[1] + bbox[3]) / 2; + double latErr = bbox[2] - lat; + double lonErr = bbox[3] - lon; + return { + 'latitude': lat, + 'longitude': lon, + 'latitudeError': latErr, + 'longitudeError': lonErr, + }; + } + + /// + /// Neighbor + /// + /// Find neighbor of a geohash string in certain direction. + /// Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest. + /// + /// direction [lat, lon], i.e. + /// [1,0] - north + /// [1,1] - northeast + String neighbor(String hashString, var direction) { + var lonLat = decode(hashString); + var neighborLat = lonLat['latitude']! + direction[0] * lonLat['latitudeError'] * 2; + var neighborLon = lonLat['longitude']! + direction[1] * lonLat['longitudeError'] * 2; + return encode(neighborLat, neighborLon, hashString.length); + } + + /// + /// Neighbors + /// Returns all neighbors' hashstrings clockwise from north around to northwest + /// 7 0 1 + /// 6 X 2 + /// 5 4 3 + List neighbors(String hashString) { + int hashStringLength = hashString.length; + var lonlat = decode(hashString); + double? lat = lonlat['latitude']; + double? lon = lonlat['longitude']; + double latErr = lonlat['latitudeError']! * 2; + double lonErr = lonlat['longitudeError']! * 2; + + num neighborLat, neighborLon; + + String encodeNeighbor(num neighborLatDir, num neighborLonDir) { + neighborLat = lat! + neighborLatDir * latErr; + neighborLon = lon! + neighborLonDir * lonErr; + return encode(neighborLat, neighborLon, hashStringLength); + } + + var neighborHashList = [ + encodeNeighbor(1, 0), + encodeNeighbor(1, 1), + encodeNeighbor(0, 1), + encodeNeighbor(-1, 1), + encodeNeighbor(-1, 0), + encodeNeighbor(-1, -1), + encodeNeighbor(0, -1), + encodeNeighbor(1, -1) + ]; + + return neighborHashList; + } + + static int setPrecision(double km) { + /* + * 1 ≤ 5,000km × 5,000km + * 2 ≤ 1,250km × 625km + * 3 ≤ 156km × 156km + * 4 ≤ 39.1km × 19.5km + * 5 ≤ 4.89km × 4.89km + * 6 ≤ 1.22km × 0.61km + * 7 ≤ 153m × 153m + * 8 ≤ 38.2m × 19.1m + * 9 ≤ 4.77m × 4.77m + * + */ + + if (km <= 0.00477) { + return 9; + } else if (km <= 0.0382) { + return 8; + } else if (km <= 0.153) { + return 7; + } else if (km <= 1.22) { + return 6; + } else if (km <= 4.89) { + return 5; + } else if (km <= 39.1) { + return 4; + } else if (km <= 156) { + return 3; + } else if (km <= 1250) { + return 2; + } else { + return 1; + } + } + + static const double maxSupportedRadius = 8587; + + // Length of a degree latitude at the equator + static const double metersPerDegreeLatitude = 110574; + + // The equatorial circumference of the earth in meters + static const double earthMeridionalCircumference = 40007860; + + // The equatorial radius of the earth in meters + static const double earthEqRadius = 6378137; + + // The meridional radius of the earth in meters + static const double earthPolarRadius = 6357852.3; + + /* The following value assumes a polar radius of + * r_p = 6356752.3 + * and an equatorial radius of + * r_e = 6378137 + * The value is calculated as e2 == (r_e^2 - r_p^2)/(r_e^2) + * Use exact value to avoid rounding errors + */ + static const double earthE2 = 0.00669447819799; + + // Cutoff for floating point calculations + static const double epsilon = 1e-12; + + /// distance in km + static double kmDistance(Coordinates location1, Coordinates location2) { + return kmCalcDistance(location1.latitude, location1.longitude, location2.latitude, location2.longitude); + } + + /// distance in km + static double kmCalcDistance(double lat1, double long1, double lat2, double long2) { + // Earth's mean radius in meters + const radius = (earthEqRadius + earthPolarRadius) / 2; + double latDelta = _toRadians(lat1 - lat2); + double lonDelta = _toRadians(long1 - long2); + + double a = (sin(latDelta / 2) * sin(latDelta / 2)) + (cos(_toRadians(lat1)) * cos(_toRadians(lat2)) * sin(lonDelta / 2) * sin(lonDelta / 2)); + double distance = radius * 2 * atan2(sqrt(a), sqrt(1 - a)) / 1000; + return double.parse(distance.toStringAsFixed(3)); + } + + static double _toRadians(double num) { + return num * (pi / 180.0); + } +} diff --git a/lib/widget/gradiant_text.dart b/lib/widget/gradiant_text.dart new file mode 100644 index 0000000..d9423df --- /dev/null +++ b/lib/widget/gradiant_text.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class GradientText extends StatelessWidget { + const GradientText( + this.text, { + super.key, + required this.gradient, + this.style, + }); + + final String text; + final TextStyle? style; + final Gradient gradient; + + @override + Widget build(BuildContext context) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) => gradient.createShader( + Rect.fromLTWH(0, 0, bounds.width, bounds.height), + ), + child: Text(text, style: style), + ); + } +} diff --git a/lib/widget/my_separator.dart b/lib/widget/my_separator.dart new file mode 100644 index 0000000..04713c7 --- /dev/null +++ b/lib/widget/my_separator.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class MySeparator extends StatelessWidget { + const MySeparator({super.key, this.height = 1, this.color = Colors.black}); + + final double height; + final Color color; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final boxWidth = constraints.constrainWidth(); + const dashWidth = 5.0; + final dashHeight = height; + final dashCount = (boxWidth / (2 * dashWidth)).floor(); + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: dashHeight, + child: DecoratedBox( + decoration: BoxDecoration(color: color), + ), + ); + }), + ); + }, + ); + } +} diff --git a/lib/widget/osm_map/map_controller.dart b/lib/widget/osm_map/map_controller.dart new file mode 100644 index 0000000..182b786 --- /dev/null +++ b/lib/widget/osm_map/map_controller.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; +import 'package:customer/widget/osm_map/place_model.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import '../../utils/utils.dart'; +import 'package:latlong2/latlong.dart'; + +class OSMMapController extends GetxController { + final mapController = MapController(); + // Store only one picked place instead of multiple + var pickedPlace = Rxn(); // Use Rxn to hold a nullable value + var searchResults = [].obs; + + Future searchPlace(String query) async { + if (query.length < 3) { + searchResults.clear(); + return; + } + + final url = Uri.parse( + 'https://nominatim.openstreetmap.org/search?q=$query&format=json&addressdetails=1&limit=10'); + + final response = await http.get(url, headers: { + 'User-Agent': 'FlutterMapApp/1.0 (menil.siddhiinfosoft@gmail.com)', + }); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + searchResults.value = data; + } + } + + void selectSearchResult(Map place) { + final lat = double.parse(place['lat']); + final lon = double.parse(place['lon']); + final address = place['display_name']; + + // Store only the selected place + pickedPlace.value = PlaceModel( + coordinates: LatLng(lat, lon), + address: address, + ); + searchResults.clear(); + } + + void addLatLngOnly(LatLng coords) async { + final address = await _getAddressFromLatLng(coords); + pickedPlace.value = PlaceModel(coordinates: coords, address: address); + } + + Future _getAddressFromLatLng(LatLng coords) async { + final url = Uri.parse( + 'https://nominatim.openstreetmap.org/reverse?lat=${coords.latitude}&lon=${coords.longitude}&format=json'); + + final response = await http.get(url, headers: { + 'User-Agent': 'FlutterMapApp/1.0 (menil.siddhiinfosoft@gmail.com)', + }); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['display_name'] ?? 'Unknown location'; + } else { + return 'Unknown location'; + } + } + + void clearAll() { + pickedPlace.value = null; // Clear the selected place + } + + @override + void onInit() { + // TODO: implement onInit + super.onInit(); + getCurrentLocation(); + } + + Future getCurrentLocation() async { + Position? location = await Utils.getCurrentLocation(); + LatLng latlng = + LatLng(location?.latitude ?? 0.0, location?.longitude ?? 0.0); + addLatLngOnly( + LatLng(location?.latitude ?? 0.0, location?.longitude ?? 0.0)); + mapController.move(latlng, mapController.camera.zoom); + } +} diff --git a/lib/widget/osm_map/map_picker_page.dart b/lib/widget/osm_map/map_picker_page.dart new file mode 100644 index 0000000..4e55913 --- /dev/null +++ b/lib/widget/osm_map/map_picker_page.dart @@ -0,0 +1,154 @@ +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/widget/osm_map/map_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; +import '../../controllers/theme_controller.dart'; + +class MapPickerPage extends StatelessWidget { + final OSMMapController controller = Get.put(OSMMapController()); + final TextEditingController searchController = TextEditingController(); + + MapPickerPage({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return Scaffold( + appBar: AppBar( + backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface, + centerTitle: false, + titleSpacing: 0, + title: Text("PickUp Location".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)), + ), + body: Stack( + children: [ + Obx( + () => FlutterMap( + mapController: controller.mapController, + options: MapOptions( + initialCenter: controller.pickedPlace.value?.coordinates ?? LatLng(20.5937, 78.9629), // Default India center + initialZoom: 13, + onTap: (tapPos, latlng) { + controller.addLatLngOnly(latlng); + controller.mapController.move(latlng, controller.mapController.camera.zoom); + }, + ), + children: [ + TileLayer(urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], userAgentPackageName: 'com.emart.app'), + MarkerLayer( + markers: + controller.pickedPlace.value != null + ? [Marker(point: controller.pickedPlace.value!.coordinates, width: 40, height: 40, child: const Icon(Icons.location_pin, size: 36, color: Colors.red))] + : [], + ), + ], + ), + ), + Positioned( + top: 16, + left: 16, + right: 16, + child: Column( + children: [ + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: TextField( + controller: searchController, + style: TextStyle(color: isDark ? AppThemeData.grey900 : AppThemeData.grey900), + decoration: InputDecoration( + hintText: 'Search location...'.tr, + hintStyle: TextStyle(color: isDark ? AppThemeData.grey900 : AppThemeData.grey900), + contentPadding: EdgeInsets.all(12), + border: InputBorder.none, + prefixIcon: Icon(Icons.search), + ), + onChanged: controller.searchPlace, + ), + ), + Obx(() { + if (controller.searchResults.isEmpty) { + return const SizedBox.shrink(); + } + return Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), + child: ListView.builder( + shrinkWrap: true, + itemCount: controller.searchResults.length, + itemBuilder: (context, index) { + final place = controller.searchResults[index]; + return ListTile( + title: Text(place['display_name']), + onTap: () { + controller.selectSearchResult(place); + final lat = double.parse(place['lat']); + final lon = double.parse(place['lon']); + final pos = LatLng(lat, lon); + controller.mapController.move(pos, 15); + searchController.text = place['display_name']; + }, + ); + }, + ), + ); + }), + ], + ), + ), + ], + ), + bottomNavigationBar: Obx(() { + return Container( + padding: const EdgeInsets.all(16), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.pickedPlace.value != null ? "Picked Location:".tr : "No Location Picked".tr, + style: TextStyle(color: isDark ? AppThemeData.primary300 : AppThemeData.primary300, fontFamily: AppThemeData.semiBold, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + if (controller.pickedPlace.value != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Text( + "${controller.pickedPlace.value!.address}\n(${controller.pickedPlace.value!.coordinates.latitude.toStringAsFixed(5)}, ${controller.pickedPlace.value!.coordinates.longitude.toStringAsFixed(5)})", + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: RoundedButtonFill( + title: "Confirm Location".tr, + color: AppThemeData.primary300, + textColor: AppThemeData.grey50, + height: 5, + onPress: () async { + final selected = controller.pickedPlace.value; + if (selected != null) { + Get.back(result: selected); // ✅ Return the selected place + print("Selected location: $selected"); + } + }, + ), + ), + const SizedBox(width: 10), + IconButton(icon: const Icon(Icons.delete_forever, color: Colors.red), onPressed: controller.clearAll), + ], + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/widget/osm_map/place_model.dart b/lib/widget/osm_map/place_model.dart new file mode 100644 index 0000000..1098d5f --- /dev/null +++ b/lib/widget/osm_map/place_model.dart @@ -0,0 +1,21 @@ +import 'package:latlong2/latlong.dart'; + +class PlaceModel { + final LatLng coordinates; + final String address; + + PlaceModel({required this.coordinates, required this.address}); + + factory PlaceModel.fromJson(Map json) { + return PlaceModel(coordinates: LatLng(json['lat'], json['lng']), address: json['address']); + } + + Map toJson() { + return {'lat': coordinates.latitude, 'lng': coordinates.longitude, 'address': address}; + } + + @override + String toString() { + return 'Place(lat: ${coordinates.latitude}, lng: ${coordinates.longitude}, address: $address)'; + } +} diff --git a/lib/widget/permission_dialog.dart b/lib/widget/permission_dialog.dart new file mode 100644 index 0000000..da17132 --- /dev/null +++ b/lib/widget/permission_dialog.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import '../themes/app_them_data.dart'; +import '../themes/round_button_fill.dart'; + +class PermissionDialog extends StatelessWidget { + const PermissionDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + insetPadding: const EdgeInsets.all(30), + clipBehavior: Clip.antiAliasWithSaveLayer, + child: Padding( + padding: const EdgeInsets.all(30), + child: SizedBox( + width: 500, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add_location_alt_rounded, color: Theme.of(context).primaryColor, size: 100), + const SizedBox(height: 20), + Text( + 'You denied location permission forever. Please allow location permission from your app settings and receive more accurate delivery.'.tr, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30), side: BorderSide(width: 2, color: Theme.of(context).primaryColor)), + minimumSize: const Size(1, 50), + ), + child: Text('close'.tr), + onPressed: () => Navigator.pop(context), + ), + ), + const SizedBox(width: 10), + Expanded( + child: RoundedButtonFill( + title: "Settings".tr, + color: AppThemeData.grey900, + textColor: AppThemeData.grey50, + onPress: () async { + await Geolocator.openAppSettings(); + Navigator.pop(context); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widget/place_picker/location_controller.dart b/lib/widget/place_picker/location_controller.dart new file mode 100644 index 0000000..c9eb0d6 --- /dev/null +++ b/lib/widget/place_picker/location_controller.dart @@ -0,0 +1,87 @@ +import 'package:customer/widget/place_picker/selected_location_model.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:flutter/material.dart'; + +class LocationController extends GetxController { + GoogleMapController? mapController; + var selectedLocation = Rxn(); + var selectedPlaceAddress = Rxn(); + var address = "Move the map to select a location".obs; + TextEditingController searchController = TextEditingController(); + + RxString zipCode = ''.obs; + + @override + void onInit() { + super.onInit(); + getArgument(); + getCurrentLocation(); + } + + void getArgument() { + dynamic argumentData = Get.arguments; + if (argumentData != null) { + zipCode.value = argumentData['zipCode'] ?? ''; + if (zipCode.value.isNotEmpty) { + getCoordinatesFromZipCode(zipCode.value); + } + } + update(); + } + + Future getCurrentLocation() async { + try { + Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); + selectedLocation.value = LatLng(position.latitude, position.longitude); + + if (mapController != null) { + mapController!.animateCamera(CameraUpdate.newLatLngZoom(selectedLocation.value!, 15)); + } + + await getAddressFromLatLng(selectedLocation.value!); + } catch (e) { + print("Error fetching current location: $e"); + } + } + + Future getAddressFromLatLng(LatLng latLng) async { + try { + List placemarks = await placemarkFromCoordinates(latLng.latitude, latLng.longitude); + if (placemarks.isNotEmpty) { + Placemark place = placemarks.first; + selectedPlaceAddress.value = place; + address.value = "${place.street}, ${place.locality}, ${place.administrativeArea}, ${place.country}"; + } else { + address.value = "Address not found"; + } + } catch (e) { + print("Error getting address: $e"); + address.value = "Error getting address"; + } + } + + void onMapMoved(CameraPosition position) { + selectedLocation.value = position.target; + } + + Future getCoordinatesFromZipCode(String zipCode) async { + try { + List locations = await locationFromAddress(zipCode); + if (locations.isNotEmpty) { + selectedLocation.value = LatLng(locations.first.latitude, locations.first.longitude); + } + } catch (e) { + print("Error getting coordinates for ZIP code: $e"); + } + } + + void confirmLocation() { + if (selectedLocation.value != null) { + SelectedLocationModel selectedLocationModel = SelectedLocationModel(address: selectedPlaceAddress.value, latLng: selectedLocation.value); + Get.back(result: selectedLocationModel); + } + } +} diff --git a/lib/widget/place_picker/location_picker_screen.dart b/lib/widget/place_picker/location_picker_screen.dart new file mode 100644 index 0000000..efb1fdc --- /dev/null +++ b/lib/widget/place_picker/location_picker_screen.dart @@ -0,0 +1,121 @@ +import 'package:customer/themes/app_them_data.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/themes/round_button_fill.dart'; +import 'package:customer/widget/place_picker/location_controller.dart'; +import 'package:customer/constant/constant.dart'; +import 'package:customer/controllers/theme_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_google_places_hoc081098/flutter_google_places_hoc081098.dart'; +import 'package:flutter_google_places_hoc081098/google_maps_webservice_places.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +final GoogleMapsPlaces _places = GoogleMapsPlaces(apiKey: Constant.mapAPIKey); + +class LocationPickerScreen extends StatelessWidget { + const LocationPickerScreen({super.key}); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final isDark = themeController.isDark.value; + return GetX( + init: LocationController(), + builder: (controller) { + return Scaffold( + body: Stack( + children: [ + controller.selectedLocation.value == null + ? const Center(child: CircularProgressIndicator()) + : GoogleMap( + onMapCreated: (controllers) { + controller.mapController = controllers; + }, + initialCameraPosition: CameraPosition(target: controller.selectedLocation.value!, zoom: 15), + onTap: (LatLng tappedPosition) { + controller.selectedLocation.value = tappedPosition; + controller.getAddressFromLatLng(tappedPosition); + }, + markers: + controller.selectedLocation.value == null + ? {} + : { + Marker( + markerId: const MarkerId("selected-location"), + position: controller.selectedLocation.value!, + onTap: () { + controller.getAddressFromLatLng(controller.selectedLocation.value!); + }, + ), + }, + onCameraMove: controller.onMapMoved, + onCameraIdle: () { + if (controller.selectedLocation.value != null) { + controller.getAddressFromLatLng(controller.selectedLocation.value!); + } + }, + ), + Positioned( + top: 60, + left: 16, + right: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + Get.back(); + }, + child: Container( + decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50, borderRadius: BorderRadius.circular(30)), + child: Padding(padding: const EdgeInsets.all(10), child: Icon(Icons.arrow_back_ios_new_outlined, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)), + ), + ), + SizedBox(height: 20), + GestureDetector( + onTap: () async { + Prediction? p = await PlacesAutocomplete.show(context: context, apiKey: Constant.mapAPIKey, mode: Mode.overlay, language: "en"); + if (p != null) { + final detail = await _places.getDetailsByPlaceId(p.placeId!); + final lat = detail.result.geometry!.location.lat; + final lng = detail.result.geometry!.location.lng; + final LatLng pos = LatLng(lat, lng); + controller.selectedLocation.value = pos; + controller.mapController?.animateCamera(CameraUpdate.newLatLngZoom(pos, 15)); + controller.getAddressFromLatLng(pos); + } + }, + child: Container( + width: Responsive.width(100, context), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(60)), + child: Row(children: [Icon(Icons.search), SizedBox(width: 8), Text("Search place...".tr)]), + ), + ), + ], + ), + ), + Positioned( + bottom: 100, + left: 20, + right: 20, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 5)]), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Obx(() => Text(controller.address.value, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))), + const SizedBox(height: 10), + RoundedButtonFill(title: "Confirm Location".tr, height: 5.5, color: AppThemeData.primary300, textColor: AppThemeData.grey50, onPress: () => controller.confirmLocation()), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/widget/place_picker/selected_location_model.dart b/lib/widget/place_picker/selected_location_model.dart new file mode 100644 index 0000000..062d15d --- /dev/null +++ b/lib/widget/place_picker/selected_location_model.dart @@ -0,0 +1,21 @@ +import 'package:geocoding/geocoding.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +class SelectedLocationModel { + Placemark? address; + LatLng? latLng; + + SelectedLocationModel({this.address,this.latLng}); + + SelectedLocationModel.fromJson(Map json) { + address = json['address']; + latLng = json['latLng']; + } + + Map toJson() { + final Map data = {}; + data['address'] = address; + data['latLng'] = latLng; + return data; + } +} diff --git a/lib/widget/restaurant_image_view.dart b/lib/widget/restaurant_image_view.dart new file mode 100644 index 0000000..c9468cb --- /dev/null +++ b/lib/widget/restaurant_image_view.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:customer/models/vendor_model.dart'; +import 'package:customer/themes/responsive.dart'; +import 'package:customer/utils/network_image_widget.dart'; +import 'package:flutter/material.dart'; + +class RestaurantImageView extends StatefulWidget { + final VendorModel vendorModel; + + const RestaurantImageView({super.key, required this.vendorModel}); + + @override + State createState() => _RestaurantImageViewState(); +} + +class _RestaurantImageViewState extends State { + int currentPage = 0; + + PageController pageController = PageController(initialPage: 1); + + @override + void initState() { + animateSlider(); + super.initState(); + } + + void animateSlider() { + if (widget.vendorModel.photos != null && widget.vendorModel.photos!.isNotEmpty) { + if (widget.vendorModel.photos!.length > 1) { + Timer.periodic(const Duration(seconds: 2), (Timer timer) { + if (currentPage < widget.vendorModel.photos!.length - 1) { + currentPage++; + } else { + currentPage = 0; + } + + if (pageController.hasClients) { + pageController.animateToPage( + currentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + }); + } + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: Responsive.height(20, context), + child: widget.vendorModel.photos == null || widget.vendorModel.photos!.isEmpty + ? NetworkImageWidget( + imageUrl: widget.vendorModel.photo.toString(), + fit: BoxFit.cover, + height: Responsive.height(20, context), + width: Responsive.width(100, context), + ) + : PageView.builder( + physics: const BouncingScrollPhysics(), + controller: pageController, + scrollDirection: Axis.horizontal, + allowImplicitScrolling: true, + itemCount: widget.vendorModel.photos!.length, + padEnds: false, + pageSnapping: true, + itemBuilder: (BuildContext context, int index) { + String image = widget.vendorModel.photos![index]; + return NetworkImageWidget( + imageUrl: image.toString(), + fit: BoxFit.cover, + height: Responsive.height(20, context), + width: Responsive.width(100, context), + ); + }, + ), + ); + } +} diff --git a/lib/widget/story_view/controller/story_controller.dart b/lib/widget/story_view/controller/story_controller.dart new file mode 100644 index 0000000..9636aa7 --- /dev/null +++ b/lib/widget/story_view/controller/story_controller.dart @@ -0,0 +1,37 @@ +import 'package:rxdart/rxdart.dart'; + +enum PlaybackState { pause, play, next, previous } + +/// Controller to sync playback between animated child (story) views. This +/// helps make sure when stories are paused, the animation (gifs/slides) are +/// also paused. +/// Another reason for using the controller is to place the stories on `paused` +/// state when a media is loading. +class StoryController { + /// Stream that broadcasts the playback state of the stories. + final playbackNotifier = BehaviorSubject(); + + /// Notify listeners with a [PlaybackState.pause] state + void pause() { + playbackNotifier.add(PlaybackState.pause); + } + + /// Notify listeners with a [PlaybackState.play] state + void play() { + playbackNotifier.add(PlaybackState.play); + } + + void next() { + playbackNotifier.add(PlaybackState.next); + } + + void previous() { + playbackNotifier.add(PlaybackState.previous); + } + + /// Remember to call dispose when the story screen is disposed to close + /// the notifier stream. + void dispose() { + playbackNotifier.close(); + } +} diff --git a/lib/widget/story_view/story_view.dart b/lib/widget/story_view/story_view.dart new file mode 100644 index 0000000..2b03967 --- /dev/null +++ b/lib/widget/story_view/story_view.dart @@ -0,0 +1,5 @@ +export 'controller/story_controller.dart'; +export 'utils.dart'; +export 'widgets/story_image.dart'; +export 'widgets/story_video.dart'; +export 'widgets/story_view.dart'; diff --git a/lib/widget/story_view/utils.dart b/lib/widget/story_view/utils.dart new file mode 100644 index 0000000..54a37b3 --- /dev/null +++ b/lib/widget/story_view/utils.dart @@ -0,0 +1,25 @@ +enum LoadState { loading, success, failure } + +enum Direction { up, down, left, right } + +class VerticalDragInfo { + bool cancel = false; + + Direction? direction; + + void update(double primaryDelta) { + Direction tmpDirection; + + if (primaryDelta > 0) { + tmpDirection = Direction.down; + } else { + tmpDirection = Direction.up; + } + + if (direction != null && tmpDirection != direction) { + cancel = true; + } + + direction = tmpDirection; + } +} diff --git a/lib/widget/story_view/widgets/story_image.dart b/lib/widget/story_view/widgets/story_image.dart new file mode 100644 index 0000000..c7cd2a4 --- /dev/null +++ b/lib/widget/story_view/widgets/story_image.dart @@ -0,0 +1,225 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:get/get_utils/src/extensions/internacionalization.dart'; + +import '../controller/story_controller.dart'; +import '../utils.dart'; + +/// Utitlity to load image (gif, png, jpg, etc) media just once. Resource is +/// cached to disk with default configurations of [DefaultCacheManager]. +class ImageLoader { + ui.Codec? frames; + + String url; + + Map? requestHeaders; + + LoadState state = LoadState.loading; // by default + + ImageLoader(this.url, {this.requestHeaders}); + + /// Load image from disk cache first, if not found then load from network. + /// `onComplete` is called when [imageBytes] become available. + void loadImage(VoidCallback onComplete) { + if (frames != null) { + state = LoadState.success; + onComplete(); + } + + final fileStream = DefaultCacheManager().getFileStream(url, headers: requestHeaders as Map?); + + fileStream.listen( + (fileResponse) { + if (fileResponse is! FileInfo) return; + // the reason for this is that, when the cache manager fetches + // the image again from network, the provided `onComplete` should + // not be called again + if (frames != null) { + return; + } + + final imageBytes = fileResponse.file.readAsBytesSync(); + + state = LoadState.success; + + ui.instantiateImageCodec(imageBytes).then((codec) { + frames = codec; + onComplete(); + }, onError: (error) { + state = LoadState.failure; + onComplete(); + }); + }, + onError: (error) { + state = LoadState.failure; + onComplete(); + }, + ); + } +} + +/// Widget to display animated gifs or still images. Shows a loader while image +/// is being loaded. Listens to playback states from [controller] to pause and +/// forward animated media. +class StoryImage extends StatefulWidget { + final ImageLoader imageLoader; + + final BoxFit? fit; + + final StoryController? controller; + final Widget? loadingWidget; + final Widget? errorWidget; + + StoryImage( + this.imageLoader, { + Key? key, + this.controller, + this.fit, + this.loadingWidget, + this.errorWidget, + }) : super(key: key ?? UniqueKey()); + + /// Use this shorthand to fetch images/gifs from the provided [url] + factory StoryImage.url( + String url, { + StoryController? controller, + Map? requestHeaders, + BoxFit fit = BoxFit.fitWidth, + Widget? loadingWidget, + Widget? errorWidget, + Key? key, + }) { + return StoryImage( + ImageLoader( + url, + requestHeaders: requestHeaders, + ), + controller: controller, + fit: fit, + loadingWidget: loadingWidget, + errorWidget: errorWidget, + key: key, + ); + } + + @override + State createState() => StoryImageState(); +} + +class StoryImageState extends State { + ui.Image? currentFrame; + + Timer? _timer; + + StreamSubscription? _streamSubscription; + + @override + void initState() { + super.initState(); + + if (widget.controller != null) { + _streamSubscription = widget.controller!.playbackNotifier.listen((playbackState) { + // for the case of gifs we need to pause/play + if (widget.imageLoader.frames == null) { + return; + } + + if (playbackState == PlaybackState.pause) { + _timer?.cancel(); + } else { + forward(); + } + }); + } + + widget.controller?.pause(); + + widget.imageLoader.loadImage(() async { + if (mounted) { + if (widget.imageLoader.state == LoadState.success) { + widget.controller?.play(); + forward(); + } else { + // refresh to show error + setState(() {}); + } + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + _streamSubscription?.cancel(); + + super.dispose(); + } + + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + + void forward() async { + _timer?.cancel(); + + if (widget.controller != null && widget.controller!.playbackNotifier.stream.value == PlaybackState.pause) { + return; + } + + final nextFrame = await widget.imageLoader.frames!.getNextFrame(); + + currentFrame = nextFrame.image; + + if (nextFrame.duration > const Duration(milliseconds: 0)) { + _timer = Timer(nextFrame.duration, forward); + } + + setState(() {}); + } + + Widget getContentView() { + switch (widget.imageLoader.state) { + case LoadState.success: + return RawImage( + image: currentFrame, + fit: widget.fit, + ); + case LoadState.failure: + return Center( + child: widget.errorWidget ?? + Text( + "Image failed to load.".tr, + style: TextStyle( + color: Colors.white, + ), + )); + default: + return Center( + child: widget.loadingWidget ?? + const SizedBox( + width: 70, + height: 70, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + strokeWidth: 3, + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: getContentView(), + ); + } +} diff --git a/lib/widget/story_view/widgets/story_video.dart b/lib/widget/story_view/widgets/story_video.dart new file mode 100644 index 0000000..9e74aef --- /dev/null +++ b/lib/widget/story_view/widgets/story_video.dart @@ -0,0 +1,162 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:video_player/video_player.dart'; +import '../controller/story_controller.dart'; +import '../utils.dart'; + +class VideoLoader { + String url; + + File? videoFile; + + Map? requestHeaders; + + LoadState state = LoadState.loading; + + VideoLoader(this.url, {this.requestHeaders}); + + void loadVideo(VoidCallback onComplete) { + if (videoFile != null) { + state = LoadState.success; + onComplete(); + } + + final fileStream = DefaultCacheManager().getFileStream(url, headers: requestHeaders as Map?); + + fileStream.listen((fileResponse) { + if (fileResponse is FileInfo) { + if (videoFile == null) { + state = LoadState.success; + videoFile = fileResponse.file; + onComplete(); + } + } + }); + } +} + +class StoryVideo extends StatefulWidget { + final StoryController? storyController; + final VideoLoader videoLoader; + final Widget? loadingWidget; + final Widget? errorWidget; + + StoryVideo( + this.videoLoader, { + Key? key, + this.storyController, + this.loadingWidget, + this.errorWidget, + }) : super(key: key ?? UniqueKey()); + + static StoryVideo url( + String url, { + StoryController? controller, + Map? requestHeaders, + Key? key, + Widget? loadingWidget, + Widget? errorWidget, + }) { + return StoryVideo( + VideoLoader(url, requestHeaders: requestHeaders), + storyController: controller, + key: key, + loadingWidget: loadingWidget, + errorWidget: errorWidget, + ); + } + + @override + State createState() { + return StoryVideoState(); + } +} + +class StoryVideoState extends State { + Future? playerLoader; + + StreamSubscription? _streamSubscription; + + VideoPlayerController? playerController; + + @override + void initState() { + super.initState(); + + widget.storyController!.pause(); + + widget.videoLoader.loadVideo(() { + if (widget.videoLoader.state == LoadState.success) { + playerController = VideoPlayerController.file(widget.videoLoader.videoFile!); + + playerController!.initialize().then((v) { + setState(() {}); + widget.storyController!.play(); + }); + + if (widget.storyController != null) { + _streamSubscription = widget.storyController!.playbackNotifier.listen((playbackState) { + if (playbackState == PlaybackState.pause) { + playerController!.pause(); + } else { + playerController!.play(); + } + }); + } + } else { + setState(() {}); + } + }); + } + + Widget getContentView() { + if (widget.videoLoader.state == LoadState.success && playerController!.value.isInitialized) { + return Center( + child: AspectRatio( + aspectRatio: playerController!.value.aspectRatio, + child: VideoPlayer(playerController!), + ), + ); + } + + return widget.videoLoader.state == LoadState.loading + ? Center( + child: widget.loadingWidget ?? + const SizedBox( + width: 70, + height: 70, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + strokeWidth: 3, + ), + ), + ) + : Center( + child: widget.errorWidget ?? + const Text( + "Media failed to load.", + style: TextStyle( + color: Colors.white, + ), + )); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.black, + height: double.infinity, + width: double.infinity, + child: getContentView(), + ); + } + + @override + void dispose() { + playerController?.dispose(); + _streamSubscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/widget/story_view/widgets/story_view.dart b/lib/widget/story_view/widgets/story_view.dart new file mode 100644 index 0000000..c96d214 --- /dev/null +++ b/lib/widget/story_view/widgets/story_view.dart @@ -0,0 +1,874 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; + +import '../controller/story_controller.dart'; +import '../utils.dart'; +import 'story_image.dart'; +import 'story_video.dart'; + +/// Indicates where the progress indicators should be placed. +enum ProgressPosition { top, bottom, none } + +/// This is used to specify the height of the progress indicator. Inline stories +/// should use [small] +enum IndicatorHeight { small, medium, large } + +/// This is a representation of a story item (or page). +class StoryItem { + /// Specifies how long the page should be displayed. It should be a reasonable + /// amount of time greater than 0 milliseconds. + final Duration duration; + + /// Has this page been shown already? This is used to indicate that the page + /// has been displayed. If some pages are supposed to be skipped in a story, + /// mark them as shown `shown = true`. + /// + /// However, during initialization of the story view, all pages after the + /// last unshown page will have their `shown` attribute altered to false. This + /// is because the next item to be displayed is taken by the last unshown + /// story item. + bool shown; + + /// The page content + final Widget view; + + StoryItem( + this.view, { + required this.duration, + this.shown = false, + }); + + /// Short hand to create text-only page. + /// + /// [title] is the text to be displayed on [backgroundColor]. The text color + /// alternates between [Colors.black] and [Colors.white] depending on the + /// calculated contrast. This is to ensure readability of text. + /// + /// Works for inline and full-page stories. See [StoryView.inline] for more on + /// what inline/full-page means. + static StoryItem text({ + required String title, + required Color backgroundColor, + Key? key, + TextStyle? textStyle, + bool shown = false, + bool roundedTop = false, + bool roundedBottom = false, + EdgeInsetsGeometry? textOuterPadding, + Duration? duration, + }) { + double contrast = ContrastHelper.contrast([ + backgroundColor.red, + backgroundColor.green, + backgroundColor.blue, + ], [ + 255, + 255, + 255 + ] /** white text */); + + return StoryItem( + Container( + key: key, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(roundedTop ? 8 : 0), + bottom: Radius.circular(roundedBottom ? 8 : 0), + ), + ), + padding: textOuterPadding ?? + const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + child: Center( + child: Text( + title, + style: textStyle?.copyWith( + color: contrast > 1.8 ? Colors.white : Colors.black, + ) ?? + TextStyle( + color: contrast > 1.8 ? Colors.white : Colors.black, + fontSize: 18, + ), + textAlign: TextAlign.center, + ), + ), + //color: backgroundColor, + ), + shown: shown, + duration: duration ?? const Duration(seconds: 3), + ); + } + + /// Factory constructor for page images. [controller] should be same instance as + /// one passed to the `StoryView` + factory StoryItem.pageImage({ + required String url, + required StoryController controller, + Key? key, + BoxFit imageFit = BoxFit.fitWidth, + Text? caption, + bool shown = false, + Map? requestHeaders, + Widget? loadingWidget, + Widget? errorWidget, + EdgeInsetsGeometry? captionOuterPadding, + Duration? duration, + }) { + return StoryItem( + Container( + key: key, + color: Colors.black, + child: Stack( + children: [ + StoryImage.url( + url, + controller: controller, + fit: imageFit, + requestHeaders: requestHeaders, + loadingWidget: loadingWidget, + errorWidget: errorWidget, + ), + SafeArea( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + margin: const EdgeInsets.only( + bottom: 24, + ), + padding: captionOuterPadding ?? + const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + color: caption != null ? Colors.black54 : Colors.transparent, + child: caption ?? const SizedBox.shrink(), + ), + ), + ) + ], + ), + ), + shown: shown, + duration: duration ?? const Duration(seconds: 3), + ); + } + + /// Shorthand for creating inline image. [controller] should be same instance as + /// one passed to the `StoryView` + factory StoryItem.inlineImage({ + required String url, + Text? caption, + required StoryController controller, + Key? key, + BoxFit imageFit = BoxFit.cover, + Map? requestHeaders, + bool shown = false, + bool roundedTop = true, + bool roundedBottom = false, + Widget? loadingWidget, + Widget? errorWidget, + EdgeInsetsGeometry? captionOuterPadding, + Duration? duration, + }) { + return StoryItem( + ClipRRect( + key: key, + borderRadius: BorderRadius.vertical( + top: Radius.circular(roundedTop ? 8 : 0), + bottom: Radius.circular(roundedBottom ? 8 : 0), + ), + child: Container( + color: Colors.grey[100], + child: Container( + color: Colors.black, + child: Stack( + children: [ + StoryImage.url( + url, + controller: controller, + fit: imageFit, + requestHeaders: requestHeaders, + loadingWidget: loadingWidget, + errorWidget: errorWidget, + ), + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: captionOuterPadding ?? const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Align( + alignment: Alignment.bottomLeft, + child: SizedBox( + width: double.infinity, + child: caption ?? const SizedBox.shrink(), + ), + ), + ), + ], + ), + ), + ), + ), + shown: shown, + duration: duration ?? const Duration(seconds: 3), + ); + } + + /// Shorthand for creating page video. [controller] should be same instance as + /// one passed to the `StoryView` + factory StoryItem.pageVideo( + String url, { + required StoryController controller, + Key? key, + Duration? duration, + BoxFit imageFit = BoxFit.fitWidth, + Widget? caption, + bool shown = false, + Map? requestHeaders, + Widget? loadingWidget, + Widget? errorWidget, + }) { + return StoryItem( + Container( + key: key, + color: Colors.black, + child: Stack( + children: [ + StoryVideo.url( + url, + controller: controller, + requestHeaders: requestHeaders, + loadingWidget: loadingWidget, + errorWidget: errorWidget, + ), + SafeArea( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 24), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + color: caption != null ? Colors.black54 : Colors.transparent, + child: caption ?? const SizedBox.shrink(), + ), + ), + ) + ], + ), + ), + shown: shown, + duration: duration ?? const Duration(seconds: 10)); + } + + /// Shorthand for creating a story item from an image provider such as `AssetImage` + /// or `NetworkImage`. However, the story continues to play while the image loads + /// up. + factory StoryItem.pageProviderImage( + ImageProvider image, { + Key? key, + BoxFit imageFit = BoxFit.fitWidth, + String? caption, + bool shown = false, + Duration? duration, + }) { + return StoryItem( + Container( + key: key, + color: Colors.black, + child: Stack( + children: [ + Center( + child: Image( + image: image, + height: double.infinity, + width: double.infinity, + fit: imageFit, + ), + ), + SafeArea( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + margin: const EdgeInsets.only( + bottom: 24, + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + color: caption != null ? Colors.black54 : Colors.transparent, + child: caption != null + ? Text( + caption, + style: const TextStyle( + fontSize: 15, + color: Colors.white, + ), + textAlign: TextAlign.center, + ) + : const SizedBox(), + ), + ), + ) + ], + ), + ), + shown: shown, + duration: duration ?? const Duration(seconds: 3)); + } + + /// Shorthand for creating an inline story item from an image provider such as `AssetImage` + /// or `NetworkImage`. However, the story continues to play while the image loads + /// up. + factory StoryItem.inlineProviderImage( + ImageProvider image, { + Key? key, + Text? caption, + bool shown = false, + bool roundedTop = true, + bool roundedBottom = false, + Duration? duration, + }) { + return StoryItem( + Container( + key: key, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.vertical( + top: Radius.circular(roundedTop ? 8 : 0), + bottom: Radius.circular(roundedBottom ? 8 : 0), + ), + image: DecorationImage( + image: image, + fit: BoxFit.cover, + )), + child: Container( + margin: const EdgeInsets.only( + bottom: 16, + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + child: Align( + alignment: Alignment.bottomLeft, + child: SizedBox( + width: double.infinity, + child: caption ?? const SizedBox(), + ), + ), + ), + ), + shown: shown, + duration: duration ?? const Duration(seconds: 3), + ); + } +} + +/// Widget to display stories just like Whatsapp and Instagram. Can also be used +/// inline/inside [ListView] or [Column] just like Google News app. Comes with +/// gestures to pause, forward and go to previous page. +class StoryView extends StatefulWidget { + /// The pages to displayed. + final List storyItems; + + /// Callback for when a full cycle of story is shown. This will be called + /// each time the full story completes when [repeat] is set to `true`. + final VoidCallback? onComplete; + + /// Callback for when a vertical swipe gesture is detected. If you do not + /// want to listen to such event, do not provide it. For instance, + /// for inline stories inside ListViews, it is preferrable to not to + /// provide this callback so as to enable scroll events on the list view. + final Function(Direction?)? onVerticalSwipeComplete; + + /// Callback for when a story and it index is currently being shown. + final void Function(StoryItem storyItem, int index)? onStoryShow; + + /// Where the progress indicator should be placed. + final ProgressPosition progressPosition; + + /// Should the story be repeated forever? + final bool repeat; + + /// If you would like to display the story as full-page, then set this to + /// `false`. But in case you would display this as part of a page (eg. in + /// a [ListView] or [Column]) then set this to `true`. + final bool inline; + + /// Controls the playback of the stories + final StoryController controller; + + /// Indicator Color + final Color? indicatorColor; + + /// Indicator Foreground Color + final Color? indicatorForegroundColor; + + /// Determine the height of the indicator + final IndicatorHeight indicatorHeight; + + /// Use this if you want to give outer padding to the indicator + final EdgeInsetsGeometry indicatorOuterPadding; + + const StoryView({ + super.key, + required this.storyItems, + required this.controller, + this.onComplete, + this.onStoryShow, + this.progressPosition = ProgressPosition.top, + this.repeat = false, + this.inline = false, + this.onVerticalSwipeComplete, + this.indicatorColor, + this.indicatorForegroundColor, + this.indicatorHeight = IndicatorHeight.large, + this.indicatorOuterPadding = const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + }); + + @override + State createState() { + return StoryViewState(); + } +} + +class StoryViewState extends State with TickerProviderStateMixin { + AnimationController? _animationController; + Animation? _currentAnimation; + Timer? _nextDebouncer; + + StreamSubscription? _playbackSubscription; + + VerticalDragInfo? verticalDragInfo; + + StoryItem? get _currentStory { + return widget.storyItems.firstWhereOrNull((it) => !it!.shown); + } + + Widget get _currentView { + var item = widget.storyItems.firstWhereOrNull((it) => !it!.shown); + item ??= widget.storyItems.last; + return item?.view ?? Container(); + } + + @override + void initState() { + super.initState(); + + // All pages after the first unshown page should have their shown value as + // false + final firstPage = widget.storyItems.firstWhereOrNull((it) => !it!.shown); + if (firstPage == null) { + for (var it2 in widget.storyItems) { + it2!.shown = false; + } + } else { + final lastShownPos = widget.storyItems.indexOf(firstPage); + widget.storyItems.sublist(lastShownPos).forEach((it) { + it!.shown = false; + }); + } + + _playbackSubscription = widget.controller.playbackNotifier.listen((playbackStatus) { + switch (playbackStatus) { + case PlaybackState.play: + _removeNextHold(); + _animationController?.forward(); + break; + + case PlaybackState.pause: + _holdNext(); // then pause animation + _animationController?.stop(canceled: false); + break; + + case PlaybackState.next: + _removeNextHold(); + _goForward(); + break; + + case PlaybackState.previous: + _removeNextHold(); + _goBack(); + break; + } + }); + + _play(); + } + + @override + void dispose() { + _clearDebouncer(); + + _animationController?.dispose(); + _playbackSubscription?.cancel(); + + super.dispose(); + } + + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + + void _play() { + _animationController?.dispose(); + // get the next playing page + final storyItem = widget.storyItems.firstWhere((it) { + return !it!.shown; + })!; + + final storyItemIndex = widget.storyItems.indexOf(storyItem); + + if (widget.onStoryShow != null) { + widget.onStoryShow!(storyItem, storyItemIndex); + } + + _animationController = AnimationController(duration: storyItem.duration, vsync: this); + + _animationController!.addStatusListener((status) { + if (status == AnimationStatus.completed) { + storyItem.shown = true; + if (widget.storyItems.last != storyItem) { + _beginPlay(); + } else { + // done playing + _onComplete(); + } + } + }); + + _currentAnimation = Tween(begin: 0.0, end: 1.0).animate(_animationController!); + + widget.controller.play(); + } + + void _beginPlay() { + setState(() {}); + _play(); + } + + void _onComplete() { + if (widget.onComplete != null) { + widget.controller.pause(); + widget.onComplete!(); + } + + if (widget.repeat) { + for (var it in widget.storyItems) { + it!.shown = false; + } + + _beginPlay(); + } + } + + void _goBack() { + _animationController!.stop(); + + if (_currentStory == null) { + widget.storyItems.last!.shown = false; + } + + if (_currentStory == widget.storyItems.first) { + _beginPlay(); + } else { + _currentStory!.shown = false; + int lastPos = widget.storyItems.indexOf(_currentStory); + final previous = widget.storyItems[lastPos - 1]!; + + previous.shown = false; + + _beginPlay(); + } + } + + void _goForward() { + if (_currentStory != widget.storyItems.last) { + _animationController!.stop(); + + // get last showing + final last = _currentStory; + + if (last != null) { + last.shown = true; + if (last != widget.storyItems.last) { + _beginPlay(); + } + } + } else { + // this is the last page, progress animation should skip to end + _animationController!.animateTo(1.0, duration: const Duration(milliseconds: 10)); + } + } + + void _clearDebouncer() { + _nextDebouncer?.cancel(); + _nextDebouncer = null; + } + + void _removeNextHold() { + _nextDebouncer?.cancel(); + _nextDebouncer = null; + } + + void _holdNext() { + _nextDebouncer?.cancel(); + _nextDebouncer = Timer(const Duration(milliseconds: 500), () {}); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + child: Stack( + children: [ + _currentView, + Visibility( + visible: widget.progressPosition != ProgressPosition.none, + child: Align( + alignment: widget.progressPosition == ProgressPosition.top ? Alignment.topCenter : Alignment.bottomCenter, + child: SafeArea( + bottom: widget.inline ? false : true, + // we use SafeArea here for notched and bezeles phones + child: Container( + padding: widget.indicatorOuterPadding, + child: PageBar( + widget.storyItems.map((it) => PageData(it!.duration, it.shown)).toList(), + _currentAnimation, + key: UniqueKey(), + indicatorHeight: widget.indicatorHeight, + indicatorColor: widget.indicatorColor, + indicatorForegroundColor: widget.indicatorForegroundColor, + ), + ), + ), + ), + ), + Align( + alignment: Alignment.centerRight, + heightFactor: 1, + child: GestureDetector( + onTapDown: (details) { + widget.controller.pause(); + }, + onTapCancel: () { + widget.controller.play(); + }, + onTapUp: (details) { + // if debounce timed out (not active) then continue anim + if (_nextDebouncer?.isActive == false) { + widget.controller.play(); + } else { + widget.controller.next(); + } + }, + onVerticalDragStart: widget.onVerticalSwipeComplete == null + ? null + : (details) { + widget.controller.pause(); + }, + onVerticalDragCancel: widget.onVerticalSwipeComplete == null + ? null + : () { + widget.controller.play(); + }, + onVerticalDragUpdate: widget.onVerticalSwipeComplete == null + ? null + : (details) { + verticalDragInfo ??= VerticalDragInfo(); + + verticalDragInfo!.update(details.primaryDelta!); + + // TODO: provide callback interface for animation purposes + }, + onVerticalDragEnd: widget.onVerticalSwipeComplete == null + ? null + : (details) { + widget.controller.play(); + // finish up drag cycle + if (!verticalDragInfo!.cancel && widget.onVerticalSwipeComplete != null) { + widget.onVerticalSwipeComplete!(verticalDragInfo!.direction); + } + + verticalDragInfo = null; + }, + )), + Align( + alignment: Alignment.centerLeft, + heightFactor: 1, + child: SizedBox( + width: 70, + child: GestureDetector(onTap: () { + widget.controller.previous(); + })), + ), + ], + ), + ); + } +} + +/// Capsule holding the duration and shown property of each story. Passed down +/// to the pages bar to render the page indicators. +class PageData { + Duration duration; + bool shown; + + PageData(this.duration, this.shown); +} + +/// Horizontal bar displaying a row of [StoryProgressIndicator] based on the +/// [pages] provided. +class PageBar extends StatefulWidget { + final List pages; + final Animation? animation; + final IndicatorHeight indicatorHeight; + final Color? indicatorColor; + final Color? indicatorForegroundColor; + + const PageBar( + this.pages, + this.animation, { + this.indicatorHeight = IndicatorHeight.large, + this.indicatorColor, + this.indicatorForegroundColor, + super.key, + }); + + @override + State createState() { + return PageBarState(); + } +} + +class PageBarState extends State { + double spacing = 4; + + @override + void initState() { + super.initState(); + + int count = widget.pages.length; + spacing = (count > 15) ? 2 : ((count > 10) ? 3 : 4); + + widget.animation!.addListener(() { + setState(() {}); + }); + } + + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + + bool isPlaying(PageData page) { + return widget.pages.firstWhereOrNull((it) => !it.shown) == page; + } + + @override + Widget build(BuildContext context) { + return Row( + children: widget.pages.map((it) { + return Expanded( + child: Container( + padding: EdgeInsets.only(right: widget.pages.last == it ? 0 : spacing), + child: StoryProgressIndicator( + isPlaying(it) ? widget.animation!.value : (it.shown ? 1 : 0), + indicatorHeight: widget.indicatorHeight == IndicatorHeight.large + ? 5 + : widget.indicatorHeight == IndicatorHeight.medium + ? 3 + : 2, + indicatorColor: widget.indicatorColor, + indicatorForegroundColor: widget.indicatorForegroundColor, + ), + ), + ); + }).toList(), + ); + } +} + +/// Custom progress bar. Supposed to be lighter than the +/// original [ProgressIndicator], and rounded at the sides. +class StoryProgressIndicator extends StatelessWidget { + /// From `0.0` to `1.0`, determines the progress of the indicator + final double value; + final double indicatorHeight; + final Color? indicatorColor; + final Color? indicatorForegroundColor; + + const StoryProgressIndicator( + this.value, { + super.key, + this.indicatorHeight = 5, + this.indicatorColor, + this.indicatorForegroundColor, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size.fromHeight( + indicatorHeight, + ), + foregroundPainter: IndicatorOval( + indicatorForegroundColor ?? Colors.white.withOpacity(0.8), + value, + ), + painter: IndicatorOval( + indicatorColor ?? Colors.white.withOpacity(0.4), + 1.0, + ), + ); + } +} + +class IndicatorOval extends CustomPainter { + final Color color; + final double widthFactor; + + IndicatorOval(this.color, this.widthFactor); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, size.width * widthFactor, size.height), const Radius.circular(3)), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +/// Concept source: https://stackoverflow.com/a/9733420 +class ContrastHelper { + static double luminance(int? r, int? g, int? b) { + final a = [r, g, b].map((it) { + double value = it!.toDouble() / 255.0; + return value <= 0.03928 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4); + }).toList(); + + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; + } + + static double contrast(rgb1, rgb2) { + return luminance(rgb2[0], rgb2[1], rgb2[2]) / luminance(rgb1[0], rgb1[1], rgb1[2]); + } +} diff --git a/lib/widget/video_widget.dart b/lib/widget/video_widget.dart new file mode 100644 index 0000000..7916810 --- /dev/null +++ b/lib/widget/video_widget.dart @@ -0,0 +1,159 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoWidget extends StatefulWidget { + final dynamic url; + final double width; + + const VideoWidget({super.key, this.width = 140, required this.url}); + + @override + VideoWidgetState createState() => VideoWidgetState(); +} + +class VideoWidgetState extends State { + late VideoPlayerController _controller; + late Future _initializeVideoPlayerFuture; + + @override + void initState() { + super.initState(); + _controller = widget.url is File + ? VideoPlayerController.file( + widget.url, + ) + : VideoPlayerController.network( + widget.url, + ); + + _initializeVideoPlayerFuture = _controller.initialize(); + } + + @override + void dispose() { + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.width, + height: MediaQuery.of(context).size.height, + child: FutureBuilder( + future: _initializeVideoPlayerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack(children: [ + VideoPlayer(_controller), + Center( + child: InkWell( + onTap: () { + if (_controller.value.isPlaying) { + _controller.pause(); + } else { + _controller.play(); + } + setState(() {}); + }, + child: Icon( + _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + )), + ) + ]), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + } +} + +class VideoAdvWidget extends StatefulWidget { + final dynamic url; + final double width; + final double? height; + + const VideoAdvWidget({super.key, this.height, this.width = 140, required this.url}); + + @override + VideoAdvWidgetState createState() => VideoAdvWidgetState(); +} + +class VideoAdvWidgetState extends State { + late VideoPlayerController _controller; + late Future _initializeVideoPlayerFuture; + + @override + void initState() { + super.initState(); + _controller = widget.url is File + ? VideoPlayerController.file( + widget.url, + ) + : VideoPlayerController.network( + widget.url, + ); + + _initializeVideoPlayerFuture = _controller.initialize(); + _controller.play(); + _controller.setLooping(true); + _controller.setVolume(0); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.width, + height: widget.height ?? MediaQuery.of(context).size.height, + child: FutureBuilder( + future: _initializeVideoPlayerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return ClipRRect( + borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), + child: Stack(children: [ + VideoPlayer(_controller), + Center( + child: InkWell( + onTap: () { + if (_controller.value.isPlaying) { + _controller.pause(); + } else { + _controller.play(); + } + setState(() {}); + }, + child: SizedBox( + width: 100, + height: 100, + )), + ) + ]), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..8d3bc1b --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,2058 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a" + url: "https://pub.dev" + source: hosted + version: "1.3.64" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + bottom_picker: + dependency: "direct main" + description: + name: bottom_picker + sha256: "721b95e389b1c91e387832833fdcd0fa8658a1e3f9b9d75a966af9354bb24ada" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + camera: + dependency: transitive + description: + name: camera + sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "0db8e7b161ec6cdb2219540eaa4cf599dc963929e5f8ded3b20b3acb52712fa4" + url: "https://pub.dev" + source: hosted + version: "0.6.21+1" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: a5a90297520e3b9841331161a7511626681153849c690c138e04a2b6d0af3026 + url: "https://pub.dev" + source: hosted + version: "0.9.21+3" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: ea1ef6ba79cdbed93df2d3eeef11542a90dec24dbcd9cde574926b86d7a09a10 + url: "https://pub.dev" + source: hosted + version: "2.11.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + charset: + dependency: transitive + description: + name: charset + sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + clipboard: + dependency: "direct main" + description: + name: clipboard + sha256: "1920c0337f8808be4166c5f1b236301ff381ef69633b0757c502d97f1f740102" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: fc1de79a62fe21615e9012f396070e6121838ef0d879475a4ec8320e79378208 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "2d2ee96a32ec3dd22fb682295e9bed6336e49a43f056d7841690228adca3ee7d" + url: "https://pub.dev" + source: hosted + version: "7.0.4" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "28c39f3d050bf669787ef13fa0890df2b4af236de864e2db0cc3897b857066cb" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + country_code_picker: + dependency: "direct main" + description: + name: country_code_picker + sha256: f0411f4833b6f98e8b7215f4fa3813bcc88e50f13925f70a170dbd36e3e447f5 + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_either: + dependency: transitive + description: + name: dart_either + sha256: "928895b8266ac5906eb4e2993fead563a73b17fc86eec6b40172100d56ca2507" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + date_picker_timeline: + dependency: "direct main" + description: + name: date_picker_timeline + sha256: b99308cd224ca42900aa3ba2a75213c8deff6edee35fc18905a7834e8b0d7e70 + url: "https://pub.dev" + source: hosted + version: "1.2.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + diacritic: + dependency: transitive + description: + name: diacritic + sha256: "12981945ec38931748836cd76f2b38773118d0baef3c68404bdfde9566147876" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + dio: + dependency: transitive + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + dotted_border: + dependency: "direct main" + description: + name: dotted_border + sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + dropdown_textfield: + dependency: "direct main" + description: + name: dropdown_textfield + sha256: ef8a35c52c92a563773d3efead94e5a1c162d6fe6c53974d0986aab6249928a1 + url: "https://pub.dev" + source: hosted + version: "1.2.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + eventify: + dependency: transitive + description: + name: eventify + sha256: b829429f08586cc2001c628e7499e3e3c2493a1d895fd73b00ecb23351aa5a66 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: e54fb3ba57de041d832574126a37726eedf0f57400869f1942b0ca8ce4a6e209 + url: "https://pub.dev" + source: hosted + version: "6.1.2" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "421f95dc553cb283ed9d4d140e719800c0331d49ed37b962e513c9d1d61b090b" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: a064ffee202f7d42d62e2c01775899d4ffcb83c602af07632f206acd46a0964e + url: "https://pub.dev" + source: hosted + version: "6.1.0" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398 + url: "https://pub.dev" + source: hosted + version: "3.3.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: d801605d5e0fbab64dbda49f214e8c527daa13005b655d5fa4cce9be687d51ea + url: "https://pub.dev" + source: hosted + version: "12.1.0" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: "820bc61acffe321cde4dacf410cd6313c74a265fffd5e066a780a61ddb893332" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: f8b40d1e64697fc5927c681f3e8f485d04cf85e2d6dd5c35b71e7c1fd57f0d1a + url: "https://pub.dev" + source: hosted + version: "0.2.7+1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb" + url: "https://pub.dev" + source: hosted + version: "16.0.4" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398 + url: "https://pub.dev" + source: hosted + version: "4.7.4" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: "3438f38590186010ce76ece683ebf3b842cd637f31f83a13620917d7438a58fd" + url: "https://pub.dev" + source: hosted + version: "13.0.4" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: "5d56021a9d30f7ca89559c96cc4c7250ce6ff8881382ff7238fde64a1f449e39" + url: "https://pub.dev" + source: hosted + version: "5.2.15" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: a06775d1df6dd90f5fa3fe9e221b988dcbc221e73a0f8951136536e6d5e548e6 + url: "https://pub.dev" + source: hosted + version: "3.11.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc_pattern: + dependency: transitive + description: + name: flutter_bloc_pattern + sha256: "934b42da57797a759b62659bb157625749f602239c26f13adde7c61ee65d3e3f" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_easyloading: + dependency: "direct main" + description: + name: flutter_easyloading + sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c + url: "https://pub.dev" + source: hosted + version: "3.0.5" + flutter_google_places_hoc081098: + dependency: "direct main" + description: + name: flutter_google_places_hoc081098 + sha256: "75492cf112eac0d6dc08181e2601d7c6ea867cc05077c6c1604b5d527e0f92c7" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875" + url: "https://pub.dev" + source: hosted + version: "19.5.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_osm_interface: + dependency: transitive + description: + name: flutter_osm_interface + sha256: fe7b02fee205aec861d0f08f643d4d6c49a5b9df06d3b43919fdf6699a275829 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + flutter_osm_plugin: + dependency: "direct main" + description: + name: flutter_osm_plugin + sha256: f926a711cc193fed5c9aacde79603d81cd54c93ea19d35e553b5cec062b6bb46 + url: "https://pub.dev" + source: hosted + version: "1.4.3" + flutter_osm_web: + dependency: transitive + description: + name: flutter_osm_web + sha256: "570e714db452f43480fc81f48592139a48865c75a3092a19e4c536416b154bf5" + url: "https://pub.dev" + source: hosted + version: "1.4.2" + flutter_paypal: + dependency: "direct main" + description: + name: flutter_paypal + sha256: "53e82d0931171c0885f5b8989addc4c35fe4e95acfa9eaea3b4ae784bfd41cff" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 + url: "https://pub.dev" + source: hosted + version: "2.0.30" + flutter_polyline_points: + dependency: "direct main" + description: + name: flutter_polyline_points + sha256: c775fe59fbcf1f925d611c039555c7f58ed6d9411747b7a2915bbd9c5e730a51 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + flutter_provider: + dependency: transitive + description: + name: flutter_provider + sha256: "5bc7d1e9edcf364397f312b9eb901337a644a5e4a907225bcd1d7e9b020ac914" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" + url: "https://pub.dev" + source: hosted + version: "5.2.2" + flutter_stripe: + dependency: "direct main" + description: + name: flutter_stripe + sha256: a5cbf9416ed20aa27633d106082f6711f3b1cc13f33aa72870531fb522db3fbe + url: "https://pub.dev" + source: hosted + version: "12.1.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://pub.dev" + source: hosted + version: "8.2.14" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3 + url: "https://pub.dev" + source: hosted + version: "0.2.3" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + get: + dependency: "direct main" + description: + name: get + sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a" + url: "https://pub.dev" + source: hosted + version: "4.7.3" + google_api_headers: + dependency: "direct overridden" + description: + name: google_api_headers + sha256: "6e4da0377e07634105765ab75d808c419a8518c985fdcba2c937121a5946435d" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" + url: "https://pub.dev" + source: hosted + version: "8.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: "819985697596a42e1054b5feb2f407ba1ac92262e02844a40168e742b9f36dca" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: a6c9d43f6a944ff4bae5c3deb34817970ac3d591dcd7f5bd2ea450ab9e9c514a + url: "https://pub.dev" + source: hosted + version: "2.18.2" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: ca02463b19a9abc7d31fcaf22631d021d647107467f741b917a69fa26659fd75 + url: "https://pub.dev" + source: hosted + version: "2.15.5" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: f4b9b44f7b12a1f6707ffc79d082738e0b7e194bf728ee61d2b3cdf5fdf16081 + url: "https://pub.dev" + source: hosted + version: "2.14.0" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: "53e5dbf73ff04153acc55a038248706967c21d5b6ef6657a57fce2be73c2895a" + url: "https://pub.dev" + source: hosted + version: "0.5.14+2" + google_polyline_algorithm: + dependency: transitive + description: + name: google_polyline_algorithm + sha256: "357874f00d3f93c3ba1bf4b4d9a154aa9ee87147c068238c1e8392012b686a03" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "7abdfa0088dc8f7d08eb3dbb1665a72bcb5b37afa256c9ec5d21e1e2d7503e5c" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: d9d80f953a244a099a40df1ff6aadc10ee375e6a098bbd5d55be332ce26db18c + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + googleapis_auth: + dependency: "direct main" + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: "direct overridden" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_auth: + dependency: transitive + description: + name: http_auth + sha256: b7625acba2987fa69140d9600c679819f33227d665f525fbb2f394e08cf917d1 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + in_app_review: + dependency: "direct main" + description: + name: in_app_review + sha256: ab26ac54dbd802896af78c670b265eaeab7ecddd6af4d0751e9604b60574817f + url: "https://pub.dev" + source: hosted + version: "2.0.11" + in_app_review_platform_interface: + dependency: transitive + description: + name: in_app_review_platform_interface + sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + listenable_stream: + dependency: transitive + description: + name: listenable_stream + sha256: "80decc4ef1dd999b42cf696d63f7729d1298a68f75b6bf3c944851ce5bf0eafd" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + location: + dependency: "direct main" + description: + name: location + sha256: b080053c181c7d152c43dd576eec6436c40e25f326933051c330da563ddd5333 + url: "https://pub.dev" + source: hosted + version: "8.0.1" + location_platform_interface: + dependency: transitive + description: + name: location_platform_interface + sha256: ca8700bb3f6b1e8b2afbd86bd78b2280d116c613ca7bfa1d4d7b64eba357d749 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + location_web: + dependency: transitive + description: + name: location_web + sha256: b8e3add5efe0d65c5e692b7a135d80a4015c580d3ea646fa71973e97668dd868 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + mailer: + dependency: "direct main" + description: + name: mailer + sha256: c3b934c0e800ddc946167c0123a900eba5acd009abb73648d0191a742542f2b4 + url: "https://pub.dev" + source: hosted + version: "6.6.0" + map_launcher: + dependency: "direct main" + description: + name: map_launcher + sha256: "39af937533f3d9af306357c28546ded909a0f81c76097e118724df4d0713d2f2" + url: "https://pub.dev" + source: hosted + version: "4.4.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + osm_nominatim: + dependency: "direct main" + description: + name: osm_nominatim + sha256: "058e36e0cb93f3318f3516e1ab7bdb845867370c13c5490eb3e8b3bc2ac41bf8" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + package_info_plus: + dependency: "direct overridden" + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: transitive + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" + pin_code_fields: + dependency: "direct main" + description: + name: pin_code_fields + sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" + url: "https://pub.dev" + source: hosted + version: "8.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_code_dart_decoder: + dependency: transitive + description: + name: qr_code_dart_decoder + sha256: "4044f13a071da6102f7e9bc44a6b1ce577604d7846bcbeb1be412a137b825017" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + qr_code_dart_scan: + dependency: "direct main" + description: + name: qr_code_dart_scan + sha256: "81443d940f8f27baaa4b9aeaa8d3d2155ad2c0b9842a9bacb03dab85c111e2f6" + url: "https://pub.dev" + source: hosted + version: "0.11.5" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + razorpay_flutter: + dependency: "direct main" + description: + name: razorpay_flutter + sha256: "7d86b2a2ba2c3a71366bbfb65664236ba4b12fd6aeaed4c13dfc5c998786b2d6" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + routing_client_dart: + dependency: transitive + description: + name: routing_client_dart + sha256: "4e2acf1145bb8056e04fd93101a9f2bab2fd769c9d972f7f403f9733a670cb33" + url: "https://pub.dev" + source: hosted + version: "0.5.5" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + rxdart_ext: + dependency: transitive + description: + name: rxdart_ext + sha256: "95df7e8b13140e2c3fdb3b943569a51f18090e82aaaf6ca6e8e6437e434a6fb0" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sign_in_with_apple: + dependency: "direct main" + description: + name: sign_in_with_apple + sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + stripe_android: + dependency: transitive + description: + name: stripe_android + sha256: f9127544327fe5bf23772a9899a46c4af1c2eccd128eb548c09b4ce9d3c99d7b + url: "https://pub.dev" + source: hosted + version: "12.1.0" + stripe_ios: + dependency: transitive + description: + name: stripe_ios + sha256: "82cd4c056730ce943a4f99d82433a358f8e09d871e6c71cb54bc053aa8f49a1d" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + stripe_platform_interface: + dependency: transitive + description: + name: stripe_platform_interface + sha256: "1766fa3a52a4c2fee7fa293b14655e07517d743be3f045aa3a28f8cb49629e70" + url: "https://pub.dev" + source: hosted + version: "12.1.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + timelines_plus: + dependency: "direct main" + description: + name: timelines_plus + sha256: d621d8724bc8f64957127c1195436996548e166296682d081bedcdb0abe1b638 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b" + url: "https://pub.dev" + source: hosted + version: "6.3.22" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_compress: + dependency: "direct main" + description: + name: video_compress + sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "59e5a457ddcc1688f39e9aef0efb62aa845cf0cbbac47e44ac9730dc079a2385" + url: "https://pub.dev" + source: hosted + version: "2.8.13" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd + url: "https://pub.dev" + source: hosted + version: "2.8.4" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a + url: "https://pub.dev" + source: hosted + version: "6.4.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: "direct overridden" + description: + name: webview_flutter + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + url: "https://pub.dev" + source: hosted + version: "4.9.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + url: "https://pub.dev" + source: hosted + version: "3.16.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fea63576b3b7e02b2df8b78ba92b48ed66caec2bb041e9a0b1cbd586d5d80bfd + url: "https://pub.dev" + source: hosted + version: "3.23.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + zxing_lib: + dependency: transitive + description: + name: zxing_lib + sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0 + url: "https://pub.dev" + source: hosted + version: "1.1.4" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..a196789 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,120 @@ +name: customer +description: "A new Flutter project." + +version: 1.0.0+1 + +environment: + sdk: ^3.7.2 + +dependencies: + flutter: + sdk: flutter + + # Icons & UI + cupertino_icons: ^1.0.8 + flutter_svg: ^2.2.3 + shimmer: ^3.0.0 + flutter_easyloading: ^3.0.5 + flutter_rating_bar: ^4.0.1 + dotted_border: ^3.1.0 + flutter_html: ^3.0.0 + photo_view: ^0.15.0 + bottom_picker: ^4.1.1 + timelines_plus: ^1.0.8 + badges: ^3.1.2 + in_app_review: ^2.0.11 + dropdown_textfield: ^1.2.0 + + # State Management + get: ^4.7.3 + provider: ^6.1.5+1 + + # Network & Storage + cached_network_image: ^3.4.1 + shared_preferences: ^2.5.3 + path_provider: ^2.1.5 + path: ^1.9.1 + + # Firebase & Notifications + firebase_core: ^4.2.1 + firebase_auth: ^6.1.2 + firebase_storage: ^13.0.4 + firebase_database: ^12.1.0 + cloud_firestore: ^6.1.0 + firebase_messaging: ^16.0.4 + flutter_local_notifications: ^19.5.0 + + # Authentication / User + country_code_picker: ^3.4.1 + pin_code_fields: ^8.0.1 + google_sign_in: ^7.2.0 + sign_in_with_apple: ^7.0.1 + + # Maps & Location + google_maps_flutter: ^2.14.0 + flutter_osm_plugin: ^1.4.3 + osm_nominatim: ^4.0.1 + geolocator: any + geocoding: ^4.0.0 + flutter_polyline_points: ^3.1.0 + map_launcher: ^4.4.2 + flutter_map: ^8.2.2 + latlong2: ^0.9.1 + location: ^8.0.1 + + # Media + image_picker: ^1.2.1 + video_player: ^2.10.1 + video_compress: ^3.1.4 + + # Sharing & QR + share_plus: ^12.0.1 + qr_flutter: ^4.1.0 + qr_code_dart_scan: ^0.11.5 + clipboard: ^2.0.2 + + # Payment + flutter_stripe: ^12.1.1 + razorpay_flutter: ^1.4.0 + flutter_paypal: ^0.2.1 + + # Utilities + mailer: ^6.6.0 + googleapis_auth: ^2.0.0 + date_picker_timeline: ^1.2.7 + sqflite: ^2.4.2 + intl: ^0.20.2 + uuid: ^4.5.2 + flutter_google_places_hoc081098: ^2.0.0 + +dependency_overrides: + webview_flutter: ^4.9.0 + http: ^1.6.0 + google_api_headers: ^5.0.1 + package_info_plus: ^9.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + + +flutter: + uses-material-design: true + assets: + - assets/images/ + - assets/icons/ + - assets/fonts/ + - assets/ + + fonts: + - family: EssentialSans + fonts: + - asset: assets/fonts/essential_sans_Regular.otf + weight: 600 + - asset: assets/fonts/essential_sans_medium.otf + weight: 500 + - asset: assets/fonts/essential_sans_semi_bold.otf + weight: 600 + - asset: assets/fonts/essential_sans_bold.otf + weight: 700 diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..f38ffd9 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For emart, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:customer/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}