commit 7546b7348cc855b4b6118bb9b9d0db4a546f64cb Author: mageshwaranoff Date: Thu Oct 16 11:21:52 2025 +0530 last 16-10-2025 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..369b5ec --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# 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: "8defaa71a77c16e8547abdbfad2053ce3a6e2d5b" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + - platform: android + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + - platform: ios + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + - platform: linux + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + - platform: macos + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + - platform: web + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + - platform: windows + create_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + base_revision: 8defaa71a77c16e8547abdbfad2053ce3a6e2d5b + + # 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..48a004c --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# bookmywages + +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..e381c9e --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + 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.example.bookmywages" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + 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.example.bookmywages" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + 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") + } + } +} + +flutter { + source = "../.." +} 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..7a79ff2 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/bookmywages/MainActivity.kt b/android/app/src/main/kotlin/com/example/bookmywages/MainActivity.kt new file mode 100644 index 0000000..19c12d6 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/bookmywages/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.bookmywages + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() 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-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 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-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 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-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 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-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d 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-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e 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/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..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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..ac3b479 --- /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.12-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +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.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/assets/images/Allicon.png b/assets/images/Allicon.png new file mode 100644 index 0000000..e569107 Binary files /dev/null and b/assets/images/Allicon.png differ diff --git a/assets/images/BookMypage.png b/assets/images/BookMypage.png new file mode 100644 index 0000000..a49200a Binary files /dev/null and b/assets/images/BookMypage.png differ diff --git a/assets/images/arrowbutton.png b/assets/images/arrowbutton.png new file mode 100644 index 0000000..68ca3cd Binary files /dev/null and b/assets/images/arrowbutton.png differ diff --git a/assets/images/background.png b/assets/images/background.png new file mode 100644 index 0000000..c3f5605 Binary files /dev/null and b/assets/images/background.png differ diff --git a/assets/images/banner.png b/assets/images/banner.png new file mode 100644 index 0000000..e9a6eaa Binary files /dev/null and b/assets/images/banner.png differ diff --git a/assets/images/categories.png b/assets/images/categories.png new file mode 100644 index 0000000..11690c5 Binary files /dev/null and b/assets/images/categories.png differ diff --git a/assets/images/changepass.png b/assets/images/changepass.png new file mode 100644 index 0000000..2a7077c Binary files /dev/null and b/assets/images/changepass.png differ diff --git a/assets/images/clap.png b/assets/images/clap.png new file mode 100644 index 0000000..7556432 Binary files /dev/null and b/assets/images/clap.png differ diff --git a/assets/images/clean.png b/assets/images/clean.png new file mode 100644 index 0000000..ca43f06 Binary files /dev/null and b/assets/images/clean.png differ diff --git a/assets/images/cleaning.png b/assets/images/cleaning.png new file mode 100644 index 0000000..6a65a16 Binary files /dev/null and b/assets/images/cleaning.png differ diff --git a/assets/images/drawer.png b/assets/images/drawer.png new file mode 100644 index 0000000..f317d9a Binary files /dev/null and b/assets/images/drawer.png differ diff --git a/assets/images/filtericon.png b/assets/images/filtericon.png new file mode 100644 index 0000000..af6eefc Binary files /dev/null and b/assets/images/filtericon.png differ diff --git a/assets/images/free.png b/assets/images/free.png new file mode 100644 index 0000000..7bba5c5 Binary files /dev/null and b/assets/images/free.png differ diff --git a/assets/images/freeicon.png b/assets/images/freeicon.png new file mode 100644 index 0000000..4d64b2d Binary files /dev/null and b/assets/images/freeicon.png differ diff --git a/assets/images/history.png b/assets/images/history.png new file mode 100644 index 0000000..6f6fefb Binary files /dev/null and b/assets/images/history.png differ diff --git a/assets/images/home.png b/assets/images/home.png new file mode 100644 index 0000000..22ab603 Binary files /dev/null and b/assets/images/home.png differ diff --git a/assets/images/login.png b/assets/images/login.png new file mode 100644 index 0000000..1482fc7 Binary files /dev/null and b/assets/images/login.png differ diff --git a/assets/images/logo.jpeg b/assets/images/logo.jpeg new file mode 100644 index 0000000..a272f2c Binary files /dev/null and b/assets/images/logo.jpeg differ diff --git a/assets/images/map.png b/assets/images/map.png new file mode 100644 index 0000000..01384a6 Binary files /dev/null and b/assets/images/map.png differ diff --git a/assets/images/menu.png b/assets/images/menu.png new file mode 100644 index 0000000..5941fe4 Binary files /dev/null and b/assets/images/menu.png differ diff --git a/assets/images/notify.png b/assets/images/notify.png new file mode 100644 index 0000000..f0e2ca7 Binary files /dev/null and b/assets/images/notify.png differ diff --git a/assets/images/package.png b/assets/images/package.png new file mode 100644 index 0000000..eec8047 Binary files /dev/null and b/assets/images/package.png differ diff --git a/assets/images/paid.png b/assets/images/paid.png new file mode 100644 index 0000000..a2f00ed Binary files /dev/null and b/assets/images/paid.png differ diff --git a/assets/images/paidicon.png b/assets/images/paidicon.png new file mode 100644 index 0000000..e0bb875 Binary files /dev/null and b/assets/images/paidicon.png differ diff --git a/assets/images/profileimage.png b/assets/images/profileimage.png new file mode 100644 index 0000000..423fbfb Binary files /dev/null and b/assets/images/profileimage.png differ diff --git a/assets/images/share.png b/assets/images/share.png new file mode 100644 index 0000000..307c731 Binary files /dev/null and b/assets/images/share.png differ diff --git a/assets/images/subscription.png b/assets/images/subscription.png new file mode 100644 index 0000000..98e0fa3 Binary files /dev/null and b/assets/images/subscription.png differ diff --git a/assets/images/subscriptionimage.png b/assets/images/subscriptionimage.png new file mode 100644 index 0000000..7dc91fb Binary files /dev/null and b/assets/images/subscriptionimage.png differ diff --git a/assets/images/sucess.png b/assets/images/sucess.png new file mode 100644 index 0000000..174d502 Binary files /dev/null and b/assets/images/sucess.png differ diff --git a/assets/images/vendorwelcome.png b/assets/images/vendorwelcome.png new file mode 100644 index 0000000..df48400 Binary files /dev/null and b/assets/images/vendorwelcome.png differ diff --git a/assets/images/welcomepage.png b/assets/images/welcomepage.png new file mode 100644 index 0000000..109face Binary files /dev/null and b/assets/images/welcomepage.png differ diff --git a/fonts/._Gilroy-ExtraBold.otf b/fonts/._Gilroy-ExtraBold.otf new file mode 100644 index 0000000..95955ff Binary files /dev/null and b/fonts/._Gilroy-ExtraBold.otf differ diff --git a/fonts/._Gilroy-Light.otf b/fonts/._Gilroy-Light.otf new file mode 100644 index 0000000..8d15fc0 Binary files /dev/null and b/fonts/._Gilroy-Light.otf differ diff --git a/fonts/Gilroy-Black.ttf b/fonts/Gilroy-Black.ttf new file mode 100644 index 0000000..3e1a57e Binary files /dev/null and b/fonts/Gilroy-Black.ttf differ diff --git a/fonts/Gilroy-BlackItalic.ttf b/fonts/Gilroy-BlackItalic.ttf new file mode 100644 index 0000000..6f0b4c4 Binary files /dev/null and b/fonts/Gilroy-BlackItalic.ttf differ diff --git a/fonts/Gilroy-Bold.ttf b/fonts/Gilroy-Bold.ttf new file mode 100644 index 0000000..1aea716 Binary files /dev/null and b/fonts/Gilroy-Bold.ttf differ diff --git a/fonts/Gilroy-BoldItalic.ttf b/fonts/Gilroy-BoldItalic.ttf new file mode 100644 index 0000000..6754019 Binary files /dev/null and b/fonts/Gilroy-BoldItalic.ttf differ diff --git a/fonts/Gilroy-ExtraBold.otf b/fonts/Gilroy-ExtraBold.otf new file mode 100644 index 0000000..7413e3d Binary files /dev/null and b/fonts/Gilroy-ExtraBold.otf differ diff --git a/fonts/Gilroy-ExtraBold.ttf b/fonts/Gilroy-ExtraBold.ttf new file mode 100644 index 0000000..01eb343 Binary files /dev/null and b/fonts/Gilroy-ExtraBold.ttf differ diff --git a/fonts/Gilroy-ExtraBoldItalic.ttf b/fonts/Gilroy-ExtraBoldItalic.ttf new file mode 100644 index 0000000..86000c1 Binary files /dev/null and b/fonts/Gilroy-ExtraBoldItalic.ttf differ diff --git a/fonts/Gilroy-Heavy.ttf b/fonts/Gilroy-Heavy.ttf new file mode 100644 index 0000000..726e371 Binary files /dev/null and b/fonts/Gilroy-Heavy.ttf differ diff --git a/fonts/Gilroy-HeavyItalic.ttf b/fonts/Gilroy-HeavyItalic.ttf new file mode 100644 index 0000000..12a7e55 Binary files /dev/null and b/fonts/Gilroy-HeavyItalic.ttf differ diff --git a/fonts/Gilroy-Light.otf b/fonts/Gilroy-Light.otf new file mode 100644 index 0000000..dbc0512 Binary files /dev/null and b/fonts/Gilroy-Light.otf differ diff --git a/fonts/Gilroy-Light.ttf b/fonts/Gilroy-Light.ttf new file mode 100644 index 0000000..b08db4e Binary files /dev/null and b/fonts/Gilroy-Light.ttf differ diff --git a/fonts/Gilroy-LightItalic.ttf b/fonts/Gilroy-LightItalic.ttf new file mode 100644 index 0000000..ea4bee4 Binary files /dev/null and b/fonts/Gilroy-LightItalic.ttf differ diff --git a/fonts/Gilroy-Medium.ttf b/fonts/Gilroy-Medium.ttf new file mode 100644 index 0000000..06d6a94 Binary files /dev/null and b/fonts/Gilroy-Medium.ttf differ diff --git a/fonts/Gilroy-MediumItalic.ttf b/fonts/Gilroy-MediumItalic.ttf new file mode 100644 index 0000000..9fbb898 Binary files /dev/null and b/fonts/Gilroy-MediumItalic.ttf differ diff --git a/fonts/Gilroy-Regular.ttf b/fonts/Gilroy-Regular.ttf new file mode 100644 index 0000000..ad17f71 Binary files /dev/null and b/fonts/Gilroy-Regular.ttf differ diff --git a/fonts/Gilroy-RegularItalic.ttf b/fonts/Gilroy-RegularItalic.ttf new file mode 100644 index 0000000..628a732 Binary files /dev/null and b/fonts/Gilroy-RegularItalic.ttf differ diff --git a/fonts/Gilroy-SemiBold.ttf b/fonts/Gilroy-SemiBold.ttf new file mode 100644 index 0000000..cb3cbb6 Binary files /dev/null and b/fonts/Gilroy-SemiBold.ttf differ diff --git a/fonts/Gilroy-SemiBoldItalic.ttf b/fonts/Gilroy-SemiBoldItalic.ttf new file mode 100644 index 0000000..fc82a10 Binary files /dev/null and b/fonts/Gilroy-SemiBoldItalic.ttf differ diff --git a/fonts/Gilroy-Thin.ttf b/fonts/Gilroy-Thin.ttf new file mode 100644 index 0000000..c6daeb7 Binary files /dev/null and b/fonts/Gilroy-Thin.ttf differ diff --git a/fonts/Gilroy-ThinItalic.ttf b/fonts/Gilroy-ThinItalic.ttf new file mode 100644 index 0000000..4bc3561 Binary files /dev/null and b/fonts/Gilroy-ThinItalic.ttf differ diff --git a/fonts/Gilroy-UltraLight.ttf b/fonts/Gilroy-UltraLight.ttf new file mode 100644 index 0000000..adc3e33 Binary files /dev/null and b/fonts/Gilroy-UltraLight.ttf differ diff --git a/fonts/Gilroy-UltraLightItalic.ttf b/fonts/Gilroy-UltraLightItalic.ttf new file mode 100644 index 0000000..3403fe8 Binary files /dev/null and b/fonts/Gilroy-UltraLightItalic.ttf differ 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..7c56964 --- /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 + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2db0dd3 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*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 */; }; + 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 */; }; +/* 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 */ + 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 = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + 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 */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 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 = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + 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 = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + 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 */, + ); + 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"; + }; + 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"; + }; +/* 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_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 = 12.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages; + 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; + 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.example.bookmywages.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; + 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.example.bookmywages.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; + 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.example.bookmywages.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_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 = 12.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_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 = 12.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages; + 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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages; + 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..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + 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..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} 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..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ 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/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..b5ce0e9 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Bookmywages + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + bookmywages + 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/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/consts_widgets/app_assets.dart b/lib/consts_widgets/app_assets.dart new file mode 100644 index 0000000..6b245bb --- /dev/null +++ b/lib/consts_widgets/app_assets.dart @@ -0,0 +1,32 @@ +import 'dart:core'; + +class AppAssets { + static const String flashscreen = 'assets/images/logo.jpeg'; + static const String welcome = 'assets/images/welcomepage.png'; + static const String login = 'assets/images/login.png'; + static const String history = 'assets/images/history.png'; + static const String categories = 'assets/images/categories.png'; + static const String package = 'assets/images/package.png'; + static const String home = 'assets/images/home.png'; + static const String menu = 'assets/images/menu.png'; + static const String banner = 'assets/images/banner.png'; + static const String free = 'assets/images/free.png'; + static const String paid = 'assets/images/paid.png'; + static const String clean = 'assets/images/clean.png'; + static const String subscription = 'assets/images/subscription.png'; + static const String cleaning = 'assets/images/cleaning.png'; + static const String arrowbutton = 'assets/images/arrowbutton.png'; + static const String drawer = 'assets/images/drawer.png'; + static const String background = 'assets/images/background.png'; + static const String subscriptionimage = 'assets/images/subscriptionimage.png'; + static const String allicon = 'assets/images/Allicon.png'; + static const String filtericon = 'assets/images/filtericon.png'; + static const String sucess = 'assets/images/sucess.png'; + static const String changepass = 'assets/images/changepass.png'; + static const String vendorwelcome = 'assets/images/vendorwelcome.png'; + static const String profile = 'assets/images/profileimage.png'; + static const String freeicon = 'assets/images/freeicon.png'; + static const String paidicon = 'assets/images/paidicon.png'; + static const String share = 'assets/images/share.png'; + static const String map = 'assets/images/map.png'; +} diff --git a/lib/consts_widgets/app_colors.dart b/lib/consts_widgets/app_colors.dart new file mode 100644 index 0000000..aadfdd8 --- /dev/null +++ b/lib/consts_widgets/app_colors.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../controllers/base_controller.dart'; + +class AppColors { + bool darkTheme = false; + + BaseController baseCtrl = Get.put(BaseController()); + + AppColors() { + baseCtrl.isDarkModeEnabled.listen((value) { + changeColors(); + }); + } + + void changeColors() { + if (darkTheme == true) { + dark = Colors.white; + } else { + dark = Colors.black; + } + } + + static Color dark = Colors.black; + static const Color primary = Color(0xFF0066FF); + static const Color secondprimary = Color(0xFFffffff); + static const Color thridprimary = Color(0xFF000000); + static const Color flitericoncolor = Color(0xFF625353); + static const Color hittext = Color(0xFF5E5858); + static const Color ratingstar = Color(0xFFF55B03); + static const Color flashscreen = Color(0xFFFEFAF7); + static const Color lightgray = Color(0xff555555); + + static const Color lightGrey = Color(0xFFEBE9E9); + static const Color lightWhite = Color(0xFFFAFAFA); + static const Color lightBlue = Color(0xFFE8F5FF); + static const Color purple = Color(0xFF9C34C2); + static const Color red = Color(0xFFFF0000); +} diff --git a/lib/consts_widgets/comman_button.dart b/lib/consts_widgets/comman_button.dart new file mode 100644 index 0000000..988b6d2 --- /dev/null +++ b/lib/consts_widgets/comman_button.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class CommanButton extends StatelessWidget { + final String text; + final VoidCallback onPressed; + final bool isPrimary; + final Color backgroundColor; + final Color borderColor; + final Color textColor; + final double width; + final double height; + final double borderRadius; + final TextStyle? textStyle; + + // ignore: use_super_parameters + const CommanButton({ + Key? key, + required this.text, + required this.onPressed, + this.isPrimary = true, + this.backgroundColor = const Color(0xFF0066FF), + this.borderColor = const Color(0xFFAAAAAA), + this.textColor = Colors.white, + this.width = 311.1557922363281, + this.height = 64.22110748291016, + this.borderRadius = 36.18, + this.textStyle, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final Color bgColor = isPrimary ? backgroundColor : Colors.white; + final Color txtColor = isPrimary ? textColor : borderColor; + + return SizedBox( + width: width, + height: height, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: bgColor, + side: BorderSide(color: borderColor, width: 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + child: Text( + text, + style: textStyle ?? TextStyle(color: txtColor, fontSize: 16), + ), + ), + ); + } +} diff --git a/lib/consts_widgets/comman_textformfiled.dart b/lib/consts_widgets/comman_textformfiled.dart new file mode 100644 index 0000000..bdcdb0f --- /dev/null +++ b/lib/consts_widgets/comman_textformfiled.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +class CommonTextFormField extends StatelessWidget { + final String? hintText; + final TextEditingController? controller; + final String? Function(String?)? validator; + final bool obscureText; + final Widget? prefixIcon; + final Widget? suffixIcon; + final TextInputType? keyboardType; + final String? errorText; + final void Function(String)? onChanged; + final int? maxLines; + final int? minLines; + final bool expands; + + const CommonTextFormField({ + super.key, + this.hintText, + this.controller, + this.validator, + this.obscureText = false, + this.prefixIcon, + this.suffixIcon, + this.keyboardType, + this.errorText, + this.onChanged, + this.maxLines, + this.minLines, + this.expands = false, + }); + + @override + Widget build(BuildContext context) { + final bool isMultiline = (maxLines != null && maxLines! > 1) || expands; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 360, + height: isMultiline ? null : 57.89, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.28), + border: Border.all( + color: errorText != null ? Colors.red : const Color(0xFFBDBCBC), + width: 0.9, + ), + ), + child: TextFormField( + controller: controller, + validator: validator, + obscureText: obscureText, + keyboardType: keyboardType ?? + (isMultiline ? TextInputType.multiline : TextInputType.text), + onChanged: onChanged, + maxLines: expands ? null : maxLines ?? 1, + minLines: expands ? null : minLines, + expands: expands, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + hintText: hintText, + border: InputBorder.none, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + errorStyle: const TextStyle(height: 0), + ), + ), + ), + if (errorText != null) + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 4.0), + child: Text( + errorText!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + ], + ); + } +} diff --git a/lib/consts_widgets/user_flow_drawer.dart b/lib/consts_widgets/user_flow_drawer.dart new file mode 100644 index 0000000..f5b17d9 --- /dev/null +++ b/lib/consts_widgets/user_flow_drawer.dart @@ -0,0 +1,186 @@ +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class DrawerMenu extends StatelessWidget { + final String userName; + final String userImage; + + const DrawerMenu({ + super.key, + required this.userName, + required this.userImage, + }); + + @override + Widget build(BuildContext context) { + // final indexController = InheritedIndexController.of(context); + return SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: 580, + child: GestureDetector( + // This captures horizontal drag gestures for the entire drawer + onHorizontalDragEnd: (details) { + if (details.primaryVelocity! < 0) { + // Close drawer only when swiped left (negative velocity) + Navigator.of(context).pop(); + } + }, + child: Drawer( + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(50), + bottomRight: Radius.circular(50), + ), + ), + child: SafeArea( + child: Column( + children: [ + const SizedBox(height: 16), + // User Profile Section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + CircleAvatar( + radius: 24, + backgroundImage: NetworkImage(userImage), + ), + const SizedBox(width: 16), + Text( + userName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 30), + + // Menu Items - Completely removed Navigator.pop + _buildMenuItem( + icon: Icons.home, + iconColor: Colors.orange, + title: 'Dashboard', + onTap: () { + Navigator.of(context).pop(); + }, + ), + + _buildMenuItem( + icon: Icons.card_membership, + iconColor: Colors.blue, + title: 'Package/Subscription', + onTap: () { + Get.offAllNamed(RouterConts.packageList, arguments: 1); + }, + ), + + _buildMenuItem( + icon: Icons.book, + iconColor: Colors.deepPurple, + title: 'Book Services', + onTap: () { + Get.offAllNamed( + RouterConts.history, + arguments: { + 'historyTab': 0, // Enquiry list tab + }, + ); + }, + ), + + _buildMenuItem( + icon: Icons.history, + iconColor: Colors.pink, + title: 'History', + onTap: () { + Get.offAllNamed( + RouterConts.history, + arguments: { + 'historyTab': 0, // Enquiry list tab + }, + ); + }, + ), + + _buildMenuItem( + icon: Icons.notifications, + iconColor: Colors.amber[800]!, + title: 'Notification', + onTap: () { + // Only handle navigation logic here + // DO NOT close drawer + }, + ), + + _buildMenuItem( + icon: Icons.person, + iconColor: Colors.green, + title: 'My account', + onTap: () { + Get.offAllNamed(RouterConts.profilemainscreen); + }, + ), + + const Spacer(), + + // Sign Out + _buildMenuItem( + icon: Icons.exit_to_app, + iconColor: Colors.indigo, + title: 'Sign out', + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('userId'); + await prefs.remove('vendor_id'); + await prefs.remove('data'); + + // Using GetX navigation + Get.offAllNamed(RouterConts.loginpage); + }, + ), + + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ); + } + + Widget _buildMenuItem({ + required IconData icon, + required Color iconColor, + required String title, + required VoidCallback onTap, + }) { + return ListTile( + leading: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: iconColor, size: 20), + ), + title: Text( + title, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: onTap, + ); + } +} diff --git a/lib/consts_widgets/vendor_flow_drawer.dart b/lib/consts_widgets/vendor_flow_drawer.dart new file mode 100644 index 0000000..d9f1a1d --- /dev/null +++ b/lib/consts_widgets/vendor_flow_drawer.dart @@ -0,0 +1,205 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class DrawerMenuVendor extends StatelessWidget { + final String userName; + final String userImage; + + const DrawerMenuVendor({ + super.key, + required this.userName, + required this.userImage, + }); + + @override + Widget build(BuildContext context) { + // final indexController = InheritedIndex1Controller.of(context); + return SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: 600, + child: GestureDetector( + // This captures horizontal drag gestures for the entire drawer + onHorizontalDragEnd: (details) { + if (details.primaryVelocity! < 0) { + // Close drawer only when swiped left (negative velocity) + Navigator.of(context).pop(); + } + }, + child: Drawer( + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(50), + bottomRight: Radius.circular(50), + ), + ), + child: SafeArea( + child: Column( + children: [ + // User Profile Section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + CircleAvatar( + radius: 24, + backgroundImage: NetworkImage(userImage), + ), + const SizedBox(width: 16), + Text( + userName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + // Menu Items - Fixed navigation logic + _buildMenuItem( + icon: Icons.home, + iconColor: Colors.orange, + title: 'Dashboard', + onTap: () { + Navigator.of(context).pop(); + }, + ), + + _buildMenuItem( + icon: Icons.card_membership, + iconColor: Colors.blue, + title: 'Package/Subscription', + onTap: () { + Get.offAllNamed(RouterConts.vendorpackage, arguments: 1); + }, + ), + + _buildMenuItem( + icon: Icons.book, + iconColor: Colors.deepPurple, + title: 'Book Services', + onTap: () { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 0, // Enquiry list tab + }, + ); + }, + ), + + _buildMenuItem( + icon: Icons.payment, + iconColor: Colors.pink, + title: 'Payment', + onTap: () { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 2, // Enquiry list tab + }, + ); + }, + ), + + _buildMenuItem( + icon: Icons.person_add_alt_rounded, + iconColor: Colors.amber[800]!, + title: 'Service Management', + onTap: () { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 1, // Enquiry list tab + }, + ); + }, + ), + + _buildMenuItem( + icon: Icons.person, + iconColor: AppColors.primary, + title: 'Enquiry Management', + onTap: () { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 4, // Enquiry list tab + }, + ); + }, + ), + + _buildMenuItem( + icon: Icons.person, + iconColor: Colors.green, + title: 'My account', + onTap: () { + Navigator.of(context).pop(); + Get.toNamed(RouterConts.profilemainvendor); + }, + ), + + const Spacer(), + + // Sign Out + _buildMenuItem( + icon: Icons.exit_to_app, + iconColor: Colors.indigo, + title: 'Sign out', + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('userId'); + await prefs.remove('vendor_id'); + await prefs.remove('data'); + + // Using GetX navigation + Get.offAllNamed(RouterConts.loginpage); + }, + ), + + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ); + } + + Widget _buildMenuItem({ + required IconData icon, + required Color iconColor, + required String title, + required VoidCallback onTap, + }) { + return ListTile( + leading: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: iconColor, size: 20), + ), + title: Text( + title, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: onTap, + ); + } +} diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart new file mode 100644 index 0000000..4c2e00a --- /dev/null +++ b/lib/controllers/base_controller.dart @@ -0,0 +1,17 @@ +import 'package:get/get.dart'; + +class BaseController extends GetxController { + RxBool isDarkModeEnabled = false.obs; + RxInt currentIndex = 0.obs; + String? fbUserId; + + RxMap filterData = {}.obs; + + void updateFilterData(Map newData) { + filterData.assignAll(newData); + } + + Map getFilterData() { + return filterData; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..4dda520 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,22 @@ +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:bookmywages/routers/router.dart'; // Your actual routes file + +void main() { + runApp(const ProviderScope(child: BookMyWage())); +} + +class BookMyWage extends StatelessWidget { + const BookMyWage({super.key}); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + debugShowCheckedModeBanner: false, + initialRoute: RouterConts.flashScreen, + getPages: AppRoutes.routes, // This should match your routes file + ); + } +} \ No newline at end of file diff --git a/lib/model/Banner_model.dart b/lib/model/Banner_model.dart new file mode 100644 index 0000000..fb8fb54 --- /dev/null +++ b/lib/model/Banner_model.dart @@ -0,0 +1,32 @@ +class BannerModel { + final int id; + + final String documentUrl; // Added to store the full URL + final String createdDate; + + BannerModel({ + required this.id, + + required this.documentUrl, + required this.createdDate, + }); + + // Fixed factory method + factory BannerModel.fromJson(Map json) { + return BannerModel( + id: json['id'] as int, + + documentUrl: + json['document1'] + as String, // Use the document1 field which contains the full URL + createdDate: json['created_date'] as String, + ); + } + + Map toJson() { + return {'id': id, 'document1': documentUrl, 'created_date': createdDate}; + } + + // Convenience method to get the image URL if needed + String get imageUrl => documentUrl; +} diff --git a/lib/model/Booking_model.dart b/lib/model/Booking_model.dart new file mode 100644 index 0000000..7d48662 --- /dev/null +++ b/lib/model/Booking_model.dart @@ -0,0 +1,48 @@ +// ignore: file_names +class BookingModel { + final String name; + final String mobileNumber; + final String email; + final String message; + final String serviceDate; + final String serviceTime; + final String serviceId; + final String address; + + BookingModel({ + required this.name, + required this.mobileNumber, + required this.email, + required this.message, + required this.serviceDate, + required this.serviceTime, + required this.serviceId, + required this.address, + }); + + factory BookingModel.fromJson(Map json) { + return BookingModel( + name: json['name'] ?? '', + mobileNumber: json['mobile_number'] ?? '', + email: json['email'] ?? '', + message: json['message'] ?? '', + serviceDate: json['service_date'] ?? '', + serviceTime: json['service_time'] ?? '', + serviceId: json['service_id'] ?? '', + address: json['address'] ?? '', + ); + } + + Map toJson() { + return { + 'name': name, + 'mobile_number': mobileNumber, + 'email': email, + 'message': message, + 'service_date': serviceDate, + 'service_time': serviceTime, + 'service_id': serviceId, + 'address': address, + }; + } +} diff --git a/lib/model/Categories_model.dart b/lib/model/Categories_model.dart new file mode 100644 index 0000000..54ef55d --- /dev/null +++ b/lib/model/Categories_model.dart @@ -0,0 +1,47 @@ +class CategoriesModel { + final int id; + final String name; + // Icon filename + final String iconUrl; // Full icon URL + // Image filename + final String imageUrl; // Full image URL + final String createdDate; + + CategoriesModel({ + required this.id, + required this.name, + + required this.iconUrl, + + required this.imageUrl, + required this.createdDate, + }); + + factory CategoriesModel.fromJson(Map json) { + return CategoriesModel( + id: json['id'], + name: json['name'], + + iconUrl: json['icon1'] ?? '', + + imageUrl: json['image1'] ?? '', + createdDate: json['created_date'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + + 'icon1': iconUrl, + + 'image1': imageUrl, + 'created_date': createdDate, + }; + } + + // Convenience getters to access the image URLs + String getIconUrl() => iconUrl; + String getImageUrl() => imageUrl; +} diff --git a/lib/model/booking_modify_model.dart b/lib/model/booking_modify_model.dart new file mode 100644 index 0000000..b0ffc9b --- /dev/null +++ b/lib/model/booking_modify_model.dart @@ -0,0 +1,13 @@ +class BookingModifyModel { + final String id; + final String serviceId; + final String servicedate; + final String servicetime; + + BookingModifyModel({ + required this.id, + required this.serviceId, + required this.servicedate, + required this.servicetime, + }); +} \ No newline at end of file diff --git a/lib/model/cancel_booking.dart b/lib/model/cancel_booking.dart new file mode 100644 index 0000000..3ef5410 --- /dev/null +++ b/lib/model/cancel_booking.dart @@ -0,0 +1,11 @@ +class CancelBookingRequest { + final String id; + final String serviceId; + final String type; + + CancelBookingRequest({ + required this.id, + required this.serviceId, + required this.type, + }); +} diff --git a/lib/model/detail_page_model.dart b/lib/model/detail_page_model.dart new file mode 100644 index 0000000..79695a4 --- /dev/null +++ b/lib/model/detail_page_model.dart @@ -0,0 +1,150 @@ +class DetailPageModel { + int id; + int vendorId; + int serviceType; + String vendorname; + String servicename; + int category; + int subcategory; + String workinghours; + String workingduration; + String amount; + String location; + String description; + String details; + String averageReview; + List? videos; + String createdDate; + String vendorName; + String? profilePic; + String categoryName; + String subcategoryName; + List images1; + + DetailPageModel({ + required this.id, + required this.vendorId, + required this.serviceType, + required this.vendorname, + required this.servicename, + required this.category, + required this.subcategory, + required this.workinghours, + required this.workingduration, + required this.amount, + required this.location, + required this.description, + required this.details, + required this.averageReview, + this.videos, + required this.createdDate, + required this.vendorName, + this.profilePic, + required this.categoryName, + required this.subcategoryName, + required this.images1, + }); + + factory DetailPageModel.fromJson(Map json) { + // Helper function to safely convert to string + String safeString(dynamic value) { + if (value == null) return ''; + return value.toString(); + } + + // Helper function to safely convert to int + int safeInt(dynamic value) { + if (value == null) return 0; + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; + } + + // Helper function to safely handle videos + List? safeVideos(dynamic value) { + if (value == null) return null; + if (value is String && value.isNotEmpty) return [value]; + if (value is List) { + return value + .map((e) => e?.toString() ?? '') + .where((e) => e.isNotEmpty) + .toList(); + } + return null; + } + + // Helper function to safely handle images + List safeImages(dynamic value) { + if (value == null) return []; + if (value is List) { + return value + .map((e) => e?.toString() ?? '') + .where((e) => e.isNotEmpty) + .toList(); + } + if (value is String && value.isNotEmpty) return [value]; + return []; + } + + return DetailPageModel( + id: safeInt(json['id']), + vendorId: safeInt(json['vendor_id']), + serviceType: safeInt(json['service_type']), + vendorname: safeString(json['vendorname']), + servicename: safeString(json['servicename']), + category: safeInt(json['category']), + subcategory: safeInt(json['subcategory']), + workinghours: safeString(json['workinghours']), + workingduration: safeString(json['workingduration']), + amount: safeString(json['amount']), + location: safeString(json['location']), + description: safeString(json['description']), + details: safeString(json['details']), + averageReview: safeString(json['average_review']), + videos: safeVideos(json['videos']), + createdDate: safeString(json['created_date']), + vendorName: safeString(json['vendor_name']), + profilePic: json['profile_pic']?.toString(), + categoryName: safeString(json['category_name']), + subcategoryName: safeString(json['subcategory_name']), + images1: safeImages(json['images1']), + ); + } + + Map toJson() { + return { + 'id': id, + 'vendor_id': vendorId, + 'service_type': serviceType, + 'vendorname': vendorname, + 'servicename': servicename, + 'category': category, + 'subcategory': subcategory, + 'workinghours': workinghours, + 'workingduration': workingduration, + 'amount': amount, + 'location': location, + 'description': description, + 'details': details, + 'average_review': averageReview, + 'videos': videos, + 'created_date': createdDate, + 'vendor_name': vendorName, + 'profile_pic': profilePic, + 'category_name': categoryName, + 'subcategory_name': subcategoryName, + 'images1': images1, + }; + } + + // Helper method to get a safe display value + String getDisplayValue(String value) { + return value.isEmpty ? 'N/A' : value; + } + + // Helper method to check if images are available + bool get hasImages => images1.isNotEmpty; + + // Helper method to check if videos are available + bool get hasVideos => videos != null && videos!.isNotEmpty; +} diff --git a/lib/model/enquriy_list_model.dart b/lib/model/enquriy_list_model.dart new file mode 100644 index 0000000..ad5bf9a --- /dev/null +++ b/lib/model/enquriy_list_model.dart @@ -0,0 +1,62 @@ +class EnquiryListModel { + final int id; + final String name; + final String mobile; + final String email; + final int serviceId; + final int userId; + final String message; + final String createdDate; + final String serviceName; + + final String vendorName; + final List images1; + + EnquiryListModel({ + required this.id, + required this.name, + required this.mobile, + required this.email, + required this.serviceId, + required this.userId, + required this.message, + required this.createdDate, + required this.serviceName, + + required this.vendorName, + required this.images1, + }); + + factory EnquiryListModel.fromJson(Map json) { + return EnquiryListModel( + id: json['id'] ?? 0, + name: json['name'] ?? '', + mobile: json['mobile'] ?? '', + email: json['email'] ?? '', + serviceId: json['service_id'] ?? 0, + userId: json['user_id'] ?? 0, + message: json['message'] ?? '', + createdDate: json['created_date'] ?? '', + serviceName: json['service_name'] ?? '', + vendorName: json['vendorname'] ?? '', + images1: List.from(json['images1'] ?? []), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'mobile': mobile, + 'email': email, + 'service_id': serviceId, + 'user_id': userId, + 'message': message, + 'created_date': createdDate, + 'service_name': serviceName, + + 'vendorname': vendorName, + 'images1': images1, + }; + } +} diff --git a/lib/model/enquriy_model.dart b/lib/model/enquriy_model.dart new file mode 100644 index 0000000..6ec2272 --- /dev/null +++ b/lib/model/enquriy_model.dart @@ -0,0 +1,37 @@ +class EnquiryModel { + String? mobile; + String? name; + String? serviceId; + String? message; + String? email; + + EnquiryModel({ + this.mobile, + this.name, + this.serviceId, + this.message, + this.email, + }); + + // Factory method to create an EnquiryModel from a JSON map + factory EnquiryModel.fromJson(Map json) { + return EnquiryModel( + mobile: json['mobile'], + name: json['name'], + serviceId: json['service_id'], + message: json['message'], + email: json['email'], + ); + } + + // Method to convert EnquiryModel to a JSON map + Map toJson() { + return { + 'mobile': mobile, + 'name': name, + 'service_id': serviceId, + 'message': message, + 'email': email, + }; + } +} diff --git a/lib/model/expired_plan_model.dart b/lib/model/expired_plan_model.dart new file mode 100644 index 0000000..58220cc --- /dev/null +++ b/lib/model/expired_plan_model.dart @@ -0,0 +1,70 @@ +class ExpiredPlanModel { + final int? id; + final int? userId; + final int? type; + final int? planId; + final String? endDate; + final String? createdDate; + final String? planName; + final String? price; + final int? duration; + final String? description; + + ExpiredPlanModel({ + this.id, + this.userId, + this.type, + this.planId, + this.endDate, + this.createdDate, + this.planName, + this.price, + this.duration, + this.description, + }); + + factory ExpiredPlanModel.fromJson(Map json) { + return ExpiredPlanModel( + id: + json['id'] is int + ? json['id'] + : int.tryParse(json['id']?.toString() ?? ''), + userId: + json['user_id'] is int + ? json['user_id'] + : int.tryParse(json['user_id']?.toString() ?? ''), + type: + json['type'] is int + ? json['type'] + : int.tryParse(json['type']?.toString() ?? ''), + planId: + json['plan_id'] is int + ? json['plan_id'] + : int.tryParse(json['plan_id']?.toString() ?? ''), + endDate: json['end_date']?.toString(), + createdDate: json['created_date']?.toString(), + planName: json['plan_name']?.toString(), + price: json['price']?.toString(), + duration: + json['duration'] is int + ? json['duration'] + : int.tryParse(json['duration']?.toString() ?? ''), + description: json['description']?.toString(), + ); + } + + Map toJson() { + return { + 'id': id, + 'user_id': userId, + 'type': type, + 'plan_id': planId, + 'end_date': endDate, + 'created_date': createdDate, + 'plan_name': planName, + 'price': price, + 'duration': duration, + 'description': description, + }; + } +} diff --git a/lib/model/get_review_model.dart b/lib/model/get_review_model.dart new file mode 100644 index 0000000..4481159 --- /dev/null +++ b/lib/model/get_review_model.dart @@ -0,0 +1,47 @@ +class GetReviewModel { + final int id; + final int userId; + final int serviceId; + final String review; + final String createdDate; + final String userName; + + final String? profilePic1; + + GetReviewModel({ + required this.id, + required this.userId, + required this.serviceId, + required this.review, + required this.createdDate, + required this.userName, + + this.profilePic1, + }); + + factory GetReviewModel.fromJson(Map json) { + return GetReviewModel( + id: json['id'] ?? 0, + userId: json['user_id'] ?? 0, + serviceId: json['service_id'] ?? 0, + review: json['review'] ?? '', + createdDate: json['created_date'] ?? '', + userName: json['user_name'] ?? '', + + profilePic1: json['profile_pic1'], + ); + } + + Map toJson() { + return { + 'id': id, + 'user_id': userId, + 'service_id': serviceId, + 'review': review, + 'created_date': createdDate, + 'user_name': userName, + + 'profile_pic1': profilePic1, + }; + } +} diff --git a/lib/model/login_model.dart b/lib/model/login_model.dart new file mode 100644 index 0000000..6cb2b92 --- /dev/null +++ b/lib/model/login_model.dart @@ -0,0 +1,22 @@ +class LoginModel { + String email; + String password; + + LoginModel({required this.email, required this.password}); + + // For converting to JSON + Map toJson() { + return { + 'email': email, + 'password': password, + }; + } + + // For creating an instance from JSON + factory LoginModel.fromJson(Map json) { + return LoginModel( + email: json['email'] ?? '', + password: json['password'] ?? '', + ); + } +} diff --git a/lib/model/most_popular_model.dart b/lib/model/most_popular_model.dart new file mode 100644 index 0000000..8520bd4 --- /dev/null +++ b/lib/model/most_popular_model.dart @@ -0,0 +1,105 @@ +class MostPopularModel { + final int id; + final int vendorId; + final int serviceType; + final String vendorName; + final String? vendorNameAlt; + final String serviceName; + final int category; + final int subcategory; + final String workingHours; + final String workingDuration; + final String amount; + final String location; + final String description; + final String details; + final List? images1; // ✅ updated from String? to List? + final String? videos; + final String createdDate; + + final String? profilePic1; // ✅ new field + final double? averageReview; + final String phoneNumber; + + MostPopularModel({ + required this.id, + required this.vendorId, + required this.serviceType, + required this.vendorName, + this.vendorNameAlt, + required this.serviceName, + required this.category, + required this.subcategory, + required this.workingHours, + required this.workingDuration, + required this.amount, + required this.location, + required this.description, + required this.details, + this.images1, + this.videos, + required this.createdDate, + + this.profilePic1, // ✅ new field + this.averageReview, + required this.phoneNumber, + }); + + factory MostPopularModel.fromJson(Map json) { + return MostPopularModel( + id: json['id'] ?? 0, + vendorId: json['vendor_id'] ?? 0, + serviceType: json['service_type'] ?? 0, + vendorName: json['vendorname'] ?? '', + vendorNameAlt: json['vendor_name'], + serviceName: json['servicename'] ?? '', + category: json['category'] ?? 0, + subcategory: json['subcategory'] ?? 0, + workingHours: json['workinghours'] ?? '', + workingDuration: json['workingduration'] ?? '', + amount: json['amount']?.toString() ?? '', + location: json['location'] ?? '', + description: json['description'] ?? '', + details: json['details'] ?? '', + images1: + (json['images1'] != null) ? List.from(json['images1']) : null, + videos: json['videos'], + createdDate: json['created_date'] ?? '', + + profilePic1: json['profile_pic1'], // ✅ new field + averageReview: + json['average_review'] != null + ? (json['average_review'] is double) + ? json['average_review'] + : double.tryParse(json['average_review'].toString()) + : null, + phoneNumber: json['phone_number'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'vendor_id': vendorId, + 'service_type': serviceType, + 'vendorname': vendorName, + 'vendor_name': vendorNameAlt, + 'servicename': serviceName, + 'category': category, + 'subcategory': subcategory, + 'workinghours': workingHours, + 'workingduration': workingDuration, + 'amount': amount, + 'location': location, + 'description': description, + 'details': details, + 'images1': images1, + 'videos': videos, + 'created_date': createdDate, + + 'profile_pic1': profilePic1, // ✅ new field + 'average_review': averageReview, + 'phone_number': phoneNumber, + }; + } +} diff --git a/lib/model/notification_model.dart b/lib/model/notification_model.dart new file mode 100644 index 0000000..f430f5e --- /dev/null +++ b/lib/model/notification_model.dart @@ -0,0 +1,43 @@ +class NotificationModel { + final int id; + final int userId; + final int type; + final int serviceId; + final String message; + final bool isRead; // Add this field + final String createdDate; + final List images; + + NotificationModel({ + required this.id, + required this.userId, + required this.type, + required this.serviceId, + required this.isRead, + required this.message, + required this.createdDate, + required this.images, + }); + + factory NotificationModel.fromJson(Map json) { + // Handle both 'images' and 'images1' fields from API + List imageList = []; + + if (json['images1'] != null && json['images1'] is List) { + imageList = List.from(json['images1']); + } + + return NotificationModel( + id: json['id'] ?? 0, + userId: json['user_id'] ?? 0, + type: json['type'] ?? 0, + isRead: json['is_read'] ?? false, + serviceId: json['service_id'] ?? 0, + message: json['message'] ?? '', + createdDate: json['created_date'] ?? '', + images: imageList, + ); + } + + // ... rest of the class +} diff --git a/lib/model/otp_model.dart b/lib/model/otp_model.dart new file mode 100644 index 0000000..f9ce652 --- /dev/null +++ b/lib/model/otp_model.dart @@ -0,0 +1,13 @@ +class OtpModel { + final String userId; // Keep as String + final String otp; + + OtpModel({required this.userId, required this.otp}); + + Map toJson() { + return { + 'user_id': userId, // Send as String + 'otp': otp, + }; + } +} diff --git a/lib/model/package_model.dart b/lib/model/package_model.dart new file mode 100644 index 0000000..c0428e6 --- /dev/null +++ b/lib/model/package_model.dart @@ -0,0 +1,47 @@ +class PackageModel { + final int id; + final String name; + final String price; + final int duration; + final String description; + final int type; + final int noOfService; + final String createdDate; + + PackageModel({ + required this.id, + required this.name, + required this.price, + required this.duration, + required this.description, + required this.type, + required this.noOfService, + required this.createdDate, + }); + + factory PackageModel.fromJson(Map json) { + return PackageModel( + id: json['id'], + name: json['name'], + price: json['price'], + duration: json['duration'], + description: json['description'], + type: json['type'], + noOfService: json['no_of_service'], + createdDate: json['created_date'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'price': price, + 'duration': duration, + 'description': description, + 'type': type, + 'no_of_service': noOfService, + 'created_date': createdDate, + }; + } +} diff --git a/lib/model/payment_details_model.dart b/lib/model/payment_details_model.dart new file mode 100644 index 0000000..1aa2a4b --- /dev/null +++ b/lib/model/payment_details_model.dart @@ -0,0 +1,55 @@ +class PaymentDetailsModel { + final int id; + final int userId; + final int type; + final int planId; + final String endDate; + final String createdDate; + final String planName; + final String price; + final int duration; + final String description; + + PaymentDetailsModel({ + required this.id, + required this.userId, + required this.type, + required this.planId, + required this.endDate, + required this.createdDate, + required this.planName, + required this.price, + required this.duration, + required this.description, + }); + + factory PaymentDetailsModel.fromJson(Map json) { + return PaymentDetailsModel( + id: json['id'] ?? 0, + userId: json['user_id'] ?? 0, + type: json['type'] ?? 0, + planId: json['plan_id'] ?? 0, + endDate: json['end_date'] ?? '', + createdDate: json['created_date'] ?? '', + planName: json['plan_name'] ?? '', + price: json['price'] ?? '', + duration: json['duration'] ?? 0, + description: json['description'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'user_id': userId, + 'type': type, + 'plan_id': planId, + 'end_date': endDate, + 'created_date': createdDate, + 'plan_name': planName, + 'price': price, + 'duration': duration, + 'description': description, + }; + } +} diff --git a/lib/model/plan_sucess_model.dart b/lib/model/plan_sucess_model.dart new file mode 100644 index 0000000..38a7ce4 --- /dev/null +++ b/lib/model/plan_sucess_model.dart @@ -0,0 +1,22 @@ +class PlanSuccessModel { + final String userId; + final String planId; + final int duration; + final int type; + + PlanSuccessModel({ + required this.userId, + required this.planId, + required this.duration, + required this.type, + }); + + Map toJson() { + return { + 'user_id': userId, + 'plan_id': planId, + 'duration': duration.toString(), + 'type': type.toString(), + }; + } +} diff --git a/lib/model/profile_get_model.dart b/lib/model/profile_get_model.dart new file mode 100644 index 0000000..8656e1e --- /dev/null +++ b/lib/model/profile_get_model.dart @@ -0,0 +1,74 @@ +class ProfileGetModel { + final int id; + final String name; + final String number; + final String email; + final String password; + final int otp; + final int isVerified; + final String? address; + + final String? profilePic1; + final String? updatedAt; + final String createdAt; + final String? vendorStatus; + final String? vendorId; + final String? endDate; + + ProfileGetModel({ + required this.id, + required this.name, + required this.number, + required this.email, + required this.password, + required this.otp, + required this.isVerified, + this.address, + this.profilePic1, + this.updatedAt, + required this.createdAt, + this.vendorStatus, + this.vendorId, + this.endDate, + }); + + /// Factory method to create an instance from a JSON map + factory ProfileGetModel.fromJson(Map json) { + return ProfileGetModel( + id: int.tryParse(json['id'].toString()) ?? 0, + name: json['name']?.toString() ?? '', + number: json['number']?.toString() ?? '', + email: json['email']?.toString() ?? '', + password: json['password']?.toString() ?? '', + otp: int.tryParse(json['otp'].toString()) ?? 0, + isVerified: int.tryParse(json['is_verified'].toString()) ?? 0, + address: json['address']?.toString(), + profilePic1: json['profile_pic1']?.toString(), + updatedAt: json['updated_at']?.toString(), + createdAt: json['created_at']?.toString() ?? '', + vendorStatus: json['vendor_status']?.toString(), + vendorId: json['vendor_id']?.toString(), + endDate: json['end_date']?.toString(), + ); + } + + /// Method to convert the object to a JSON map + Map toJson() { + return { + 'id': id, + 'name': name, + 'number': number, + 'email': email, + 'password': password, + 'otp': otp, + 'is_verified': isVerified, + 'address': address, + 'profile_pic1': profilePic1, + 'updated_at': updatedAt, + 'created_at': createdAt, + 'vendor_status': vendorStatus, + 'vendor_id': vendorId, + 'end_date': endDate, + }; + } +} diff --git a/lib/model/service_model.dart b/lib/model/service_model.dart new file mode 100644 index 0000000..a70d8d2 --- /dev/null +++ b/lib/model/service_model.dart @@ -0,0 +1,118 @@ +class ServiceModel { + final int id; + final int vendorId; + final int serviceType; + final String vendorName; + final String serviceName; + final int category; + final int subcategory; + final String workingHours; + final String workingDuration; + final String amount; + final String location; + final String description; + final String details; + + final String? videos; + final String createdDate; + final String vendorDisplayName; + + final String? profilePic1; // Added for JSON field "profile_pic1" + final String? averageReview; + final String phoneNumber; + final String categoryName; + final String subcategoryName; + final List? images1; // Changed to List? + + ServiceModel({ + required this.id, + required this.vendorId, + required this.serviceType, + required this.vendorName, + required this.serviceName, + required this.category, + required this.subcategory, + required this.workingHours, + required this.workingDuration, + required this.amount, + required this.location, + required this.description, + required this.details, + + this.videos, + required this.createdDate, + required this.vendorDisplayName, + + this.profilePic1, + this.averageReview, + required this.phoneNumber, + required this.categoryName, + required this.subcategoryName, + this.images1, + }); + + factory ServiceModel.fromJson(Map json) { + return ServiceModel( + id: json['id'] ?? 0, + vendorId: json['vendor_id'] ?? 0, + serviceType: json['service_type'] ?? 0, + vendorName: json['vendorname']?.toString() ?? '', + serviceName: json['servicename']?.toString() ?? '', + category: json['category'] ?? 0, + subcategory: json['subcategory'] ?? 0, + workingHours: json['workinghours']?.toString() ?? '', + workingDuration: json['workingduration']?.toString() ?? '', + amount: json['amount']?.toString() ?? '', + location: json['location']?.toString() ?? '', + description: json['description']?.toString() ?? '', + details: json['details']?.toString() ?? '', + + videos: + (json['videos']?.toString().isNotEmpty ?? false) + ? json['videos'].toString() + : null, + createdDate: json['created_date']?.toString() ?? '', + vendorDisplayName: json['vendor_name']?.toString() ?? '', + + profilePic1: json['profile_pic1']?.toString(), // New field + averageReview: + (json['average_review']?.toString().isNotEmpty ?? false) + ? json['average_review'].toString() + : null, + phoneNumber: json['phone_number']?.toString() ?? '', + categoryName: json['category_name']?.toString() ?? '', + subcategoryName: json['subcategory_name']?.toString() ?? '', + images1: + json['images1'] != null ? List.from(json['images1']) : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'vendor_id': vendorId, + 'service_type': serviceType, + 'vendorname': vendorName, + 'servicename': serviceName, + 'category': category, + 'subcategory': subcategory, + 'workinghours': workingHours, + 'workingduration': workingDuration, + 'amount': amount, + 'location': location, + 'description': description, + 'details': details, + + 'videos': videos, + 'created_date': createdDate, + 'vendor_name': vendorDisplayName, + + 'profile_pic1': profilePic1, + 'average_review': averageReview, + 'phone_number': phoneNumber, + 'category_name': categoryName, + 'subcategory_name': subcategoryName, + 'images1': images1, + }; + } +} diff --git a/lib/model/sign_up_model.dart b/lib/model/sign_up_model.dart new file mode 100644 index 0000000..10ad623 --- /dev/null +++ b/lib/model/sign_up_model.dart @@ -0,0 +1,33 @@ +class SignUpModel { + final String name; + final String number; + final String password; + final String email; + + SignUpModel({ + required this.name, + required this.number, + required this.password, + required this.email, + }); + + // Optional: toJson for API calls + Map toJson() { + return { + 'name': name, + 'number': number, + 'password': password, + 'email': email, + }; + } + + // Optional: fromJson for parsing API responses + factory SignUpModel.fromJson(Map json) { + return SignUpModel( + name: json['name'], + number: json['number'], + password: json['password'], + email: json['email'], + ); + } +} diff --git a/lib/model/subcategory_model.dart b/lib/model/subcategory_model.dart new file mode 100644 index 0000000..c2b5f0f --- /dev/null +++ b/lib/model/subcategory_model.dart @@ -0,0 +1,39 @@ +class SubcategoryModel { + final int id; + final int category; + final String name; + + final String createdDate; + final String icon1; // Added the missing icon1 field + + SubcategoryModel({ + required this.id, + required this.category, + required this.name, + + required this.createdDate, + required this.icon1, // Added to constructor + }); + + factory SubcategoryModel.fromJson(Map json) { + return SubcategoryModel( + id: json['id'], + category: json['category'], + name: json['name'], + + createdDate: json['created_date'], + icon1: json['icon1'], // Parse icon1 from JSON + ); + } + + Map toJson() { + return { + 'id': id, + 'category': category, + 'name': name, + + 'created_date': createdDate, + 'icon1': icon1, // Include icon1 in JSON output + }; + } +} diff --git a/lib/model/user_booking_details.dart b/lib/model/user_booking_details.dart new file mode 100644 index 0000000..be326f2 --- /dev/null +++ b/lib/model/user_booking_details.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; + +class UserBookingDetails { + final int id; + final String name; + final String mobileNumber; + final String email; + final String message; + final String serviceDate; + final String serviceTime; + final int serviceId; + final int userId; + final String address; + final int status; + final int type; + final String cancelDate; + final String createdDate; + final String serviceName; + final String vendorName; + final String workingHours; + final String amount; + final List videos; + final String workingDuration; + final String description; + final String? profilePic; + final String? averageReview; + final int isRated; + final String? vendorname; + final String categoryName; + final String subcategoryName; + final List images1; + + UserBookingDetails({ + required this.id, + required this.name, + required this.mobileNumber, + required this.email, + required this.message, + required this.serviceDate, + required this.serviceTime, + required this.serviceId, + required this.userId, + required this.address, + required this.status, + required this.type, + required this.cancelDate, + required this.createdDate, + required this.serviceName, + required this.vendorName, + required this.workingHours, + required this.amount, + required this.videos, + required this.workingDuration, + required this.description, + this.profilePic, + this.averageReview, + required this.isRated, + this.vendorname, + required this.categoryName, + required this.subcategoryName, + required this.images1, + }); + + factory UserBookingDetails.fromJson(Map json) { + // Helper function to safely convert dynamic data to List + List convertToList(dynamic value) { + if (value == null) return []; + if (value is List) { + return List.from(value.map((x) => x.toString())); + } + if (value is String) { + try { + // Try to parse if it's a JSON string + final parsed = jsonDecode(value); + if (parsed is List) { + return List.from(parsed.map((x) => x.toString())); + } + return [value]; + } catch (e) { + return [value]; + } + } + return []; + } + + return UserBookingDetails( + id: json['id'] ?? 0, + name: json['name'] ?? '', + mobileNumber: json['mobile_number'] ?? '', + email: json['email'] ?? '', + message: json['message'] ?? '', + serviceDate: json['service_date'] ?? '', + serviceTime: json['service_time'] ?? '', + serviceId: json['service_id'] ?? 0, + userId: json['user_id'] ?? 0, + address: json['address'] ?? '', + status: json['status'] ?? 0, + type: json['type'] ?? 0, + cancelDate: json['cancel_date'] ?? '', + createdDate: json['created_date'] ?? '', + serviceName: json['service_name'] ?? '', + vendorName: json['vendorname'] ?? '', + workingHours: json['workinghours'] ?? '', + amount: json['amount'] ?? '', + videos: convertToList(json['videos']), + workingDuration: json['workingduration'] ?? '', + description: json['description'] ?? '', + profilePic: json['profile_pic'], + averageReview: json['average_review']?.toString(), + isRated: json['is_rated'] ?? 0, + vendorname: json['vendor_name'], + categoryName: json['category_name'] ?? '', + subcategoryName: json['subcategory_name'] ?? '', + images1: convertToList(json['images1']), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'mobile_number': mobileNumber, + 'email': email, + 'message': message, + 'service_date': serviceDate, + 'service_time': serviceTime, + 'service_id': serviceId, + 'user_id': userId, + 'address': address, + 'status': status, + 'type': type, + 'cancel_date': cancelDate, + 'created_date': createdDate, + 'service_name': serviceName, + 'vendorname': vendorName, + 'workinghours': workingHours, + 'amount': amount, + 'videos': videos, + 'workingduration': workingDuration, + 'description': description, + 'profile_pic': profilePic, + 'average_review': averageReview, + 'is_rated': isRated, + 'vendor_name': vendorname, + 'category_name': categoryName, + 'subcategory_name': subcategoryName, + 'images1': images1, + }; + } + + @override + String toString() { + return 'UserBookingDetails{id: $id, name: $name, mobileNumber: $mobileNumber, email: $email, message: $message, serviceDate: $serviceDate, serviceTime: $serviceTime, serviceId: $serviceId, userId: $userId, address: $address, status: $status, type: $type, cancelDate: $cancelDate, createdDate: $createdDate, serviceName: $serviceName, vendorName: $vendorName, workingHours: $workingHours, amount: $amount, videos: $videos, workingDuration: $workingDuration, description: $description, profilePic: $profilePic, averageReview: $averageReview, isRated: $isRated, vendor_name: $vendorname, categoryName: $categoryName, subcategoryName: $subcategoryName, images1: $images1}'; + } +} diff --git a/lib/model/vendor_model/terms_and_conditions_model.dart b/lib/model/vendor_model/terms_and_conditions_model.dart new file mode 100644 index 0000000..aa0f307 --- /dev/null +++ b/lib/model/vendor_model/terms_and_conditions_model.dart @@ -0,0 +1,19 @@ +class TermsAndConditionsModel { + final String data; + final String message; + final bool success; + + TermsAndConditionsModel({ + required this.data, + required this.message, + required this.success, + }); + + factory TermsAndConditionsModel.fromJson(Map json) { + return TermsAndConditionsModel( + data: json['data'] ?? '', + message: json['message'] ?? '', + success: json['success'] ?? false, + ); + } +} \ No newline at end of file diff --git a/lib/model/vendor_model/vendor_booking_model.dart b/lib/model/vendor_model/vendor_booking_model.dart new file mode 100644 index 0000000..ed94a86 --- /dev/null +++ b/lib/model/vendor_model/vendor_booking_model.dart @@ -0,0 +1,120 @@ +class VendorBookingModel { + int? id; + String? name; + String? mobileNumber; + String? email; + String? message; + String? serviceDate; + String? serviceTime; + int? serviceId; + int? userId; + String? address; + int? status; + String? type; + String? cancelDate; + String? createdDate; + String? serviceName; + + String? username; + String? workingHours; + String? categoryName; + String? images1; // Added missing field + String? profilePic1; // Added missing field + + VendorBookingModel({ + this.id, + this.name, + this.mobileNumber, + this.email, + this.message, + this.serviceDate, + this.serviceTime, + this.serviceId, + this.userId, + this.address, + this.status, + this.type, + this.cancelDate, + this.createdDate, + this.serviceName, + + this.username, + this.workingHours, + this.categoryName, + this.images1, // Added to constructor + this.profilePic1, // Added to constructor + }); + + VendorBookingModel.fromJson(Map json) { + // Helper function to safely convert values to int + int? toInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is String) { + try { + return int.parse(value); + } catch (e) { + print('Error parsing to int: $value'); + return null; + } + } + return null; + } + + // Helper function to safely convert values to string + String? toString(dynamic value) { + if (value == null) return null; + return value.toString(); + } + + id = toInt(json['id']); + name = toString(json['name']); + mobileNumber = toString(json['mobile_number']); + email = toString(json['email']); + message = toString(json['message']); + serviceDate = toString(json['service_date']); + serviceTime = toString(json['service_time']); + serviceId = toInt(json['service_id']); + userId = toInt(json['user_id']); + address = toString(json['address']); + status = toInt(json['status']); + type = toString(json['type']); + cancelDate = toString(json['cancel_date']); + createdDate = toString(json['created_date']); + serviceName = toString(json['service_name']); + + username = toString(json['username']); + workingHours = toString(json['workinghours']); + categoryName = toString(json['category_name']); + images1 = toString(json['images1']); // Parse images1 from JSON + profilePic1 = toString( + json['profile_pic1'], + ); // Parse profile_pic1 from JSON + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'mobile_number': mobileNumber, + 'email': email, + 'message': message, + 'service_date': serviceDate, + 'service_time': serviceTime, + 'service_id': serviceId, + 'user_id': userId, + 'address': address, + 'status': status, + 'type': type, + 'cancel_date': cancelDate, + 'created_date': createdDate, + 'service_name': serviceName, + + 'username': username, + 'workinghours': workingHours, + 'category_name': categoryName, + 'images1': images1, // Include images1 in JSON output + 'profile_pic1': profilePic1, // Include profile_pic1 in JSON output + }; + } +} diff --git a/lib/model/vendor_model/vendor_booking_status.dart b/lib/model/vendor_model/vendor_booking_status.dart new file mode 100644 index 0000000..9fbca0d --- /dev/null +++ b/lib/model/vendor_model/vendor_booking_status.dart @@ -0,0 +1,28 @@ +class VendorBookingStatus { + final String id; + final String status; + + VendorBookingStatus({ + required this.id, + required this.status, + }); + + factory VendorBookingStatus.fromJson(Map json) { + return VendorBookingStatus( + id: json['id']?.toString() ?? "0", + status: json['status']?.toString() ?? "0", + ); + } + + Map toJson() { + return { + 'id': id, + 'status': status + }; + } + + @override + String toString() { + return 'VendorBookingStatus{id: $id, status: $status}'; + } +} \ No newline at end of file diff --git a/lib/model/vendor_model/vendor_catgories_model.dart b/lib/model/vendor_model/vendor_catgories_model.dart new file mode 100644 index 0000000..7c4a0a2 --- /dev/null +++ b/lib/model/vendor_model/vendor_catgories_model.dart @@ -0,0 +1,43 @@ +class VendorCategoriesModel { + int? id; + String? name; + + String? createdDate; + int? noOfService; + String? icon1; + String? image1; + + VendorCategoriesModel({ + this.id, + this.name, + + this.createdDate, + this.noOfService, + this.icon1, + this.image1, + }); + + factory VendorCategoriesModel.fromJson(Map json) { + return VendorCategoriesModel( + id: json['id'], + name: json['name'] ?? '', + + createdDate: json['created_date'] ?? '', + noOfService: json['no_of_service'] ?? 0, + icon1: json['icon1'] ?? '', + image1: json['image1'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + + 'created_date': createdDate, + 'no_of_service': noOfService, + 'icon1': icon1, + 'image1': image1, + }; + } +} diff --git a/lib/model/vendor_model/vendor_getprofile_model.dart b/lib/model/vendor_model/vendor_getprofile_model.dart new file mode 100644 index 0000000..f9b9a55 --- /dev/null +++ b/lib/model/vendor_model/vendor_getprofile_model.dart @@ -0,0 +1,75 @@ +class VendorGetProfileModel { + final int id; + final String name; + final String email; + final String number; + final String location; + final String pincode; + final String aadharcard; + final String pancard; + final String drivingLicence; + final String passport; + final String? profilePic1; + final int status; + final int userId; + final String createdDate; + final String? endDate; + + VendorGetProfileModel({ + required this.id, + required this.name, + required this.email, + required this.number, + required this.location, + required this.pincode, + required this.aadharcard, + required this.pancard, + required this.drivingLicence, + required this.passport, + this.profilePic1, + required this.status, + required this.userId, + required this.createdDate, + this.endDate, + }); + + factory VendorGetProfileModel.fromJson(Map json) { + return VendorGetProfileModel( + id: json['id'], + name: json['name'], + email: json['email'], + number: json['number'], + location: json['location'], + pincode: json['pincode'], + aadharcard: json['aadharcard'], + pancard: json['pancard'], + drivingLicence: json['driving_licence'], + passport: json['passport'], + profilePic1: json['profile_pic1'], + status: json['status'], + userId: json['user_id'], + createdDate: json['created_date'], + endDate: json['end_date'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'email': email, + 'number': number, + 'location': location, + 'pincode': pincode, + 'aadharcard': aadharcard, + 'pancard': pancard, + 'driving_licence': drivingLicence, + 'passport': passport, + 'profile_pic1': profilePic1, + 'status': status, + 'user_id': userId, + 'created_date': createdDate, + 'end_date': endDate, + }; + } +} diff --git a/lib/model/vendor_model/vendor_profile_model.dart b/lib/model/vendor_model/vendor_profile_model.dart new file mode 100644 index 0000000..567f830 --- /dev/null +++ b/lib/model/vendor_model/vendor_profile_model.dart @@ -0,0 +1,74 @@ +class VendorProfileModel { + final int id; + final String name; + final String number; + final String email; + final String password; + final int otp; + final int isVerified; + final String? address; + + final String? profilePic1; + final String? updatedAt; + final String createdAt; + final String? vendorStatus; + final String? vendorId; + final String? endDate; + + VendorProfileModel({ + required this.id, + required this.name, + required this.number, + required this.email, + required this.password, + required this.otp, + required this.isVerified, + this.address, + this.profilePic1, + this.updatedAt, + required this.createdAt, + this.vendorStatus, + this.vendorId, + this.endDate, + }); + + /// Factory method to create an instance from a JSON map + factory VendorProfileModel.fromJson(Map json) { + return VendorProfileModel( + id: int.tryParse(json['id'].toString()) ?? 0, + name: json['name']?.toString() ?? '', + number: json['number']?.toString() ?? '', + email: json['email']?.toString() ?? '', + password: json['password']?.toString() ?? '', + otp: int.tryParse(json['otp'].toString()) ?? 0, + isVerified: int.tryParse(json['is_verified'].toString()) ?? 0, + address: json['location']?.toString(), + profilePic1: json['profile_pic1']?.toString(), + updatedAt: json['updated_at']?.toString(), + createdAt: json['created_at']?.toString() ?? '', + vendorStatus: json['vendor_status']?.toString(), + vendorId: json['vendor_id']?.toString(), + endDate: json['end_date']?.toString(), + ); + } + + /// Method to convert the object to a JSON map + Map toJson() { + return { + 'id': id, + 'name': name, + 'number': number, + 'email': email, + 'password': password, + 'otp': otp, + 'is_verified': isVerified, + 'location': address, + 'profile_pic1': profilePic1, + 'updated_at': updatedAt, + 'created_at': createdAt, + 'vendor_status': vendorStatus, + 'vendor_id': vendorId, + 'end_date': endDate, + }; + } +} diff --git a/lib/model/vendor_model/vendor_service_model.dart b/lib/model/vendor_model/vendor_service_model.dart new file mode 100644 index 0000000..f64f0ab --- /dev/null +++ b/lib/model/vendor_model/vendor_service_model.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; + +class VendorServiceModel { + final int id; + final int vendorId; + final int serviceType; + final String vendorName; + final String serviceName; + final int category; + final int subcategory; + final String workingHours; + final String workingDuration; + final String amount; + final String location; + final String description; + final String details; + final String createdDate; + final String categoryName; + final String subcategoryName; + final List video; + final List images1; + + VendorServiceModel({ + required this.id, + required this.vendorId, + required this.serviceType, + required this.vendorName, + required this.serviceName, + required this.category, + required this.subcategory, + required this.workingHours, + required this.workingDuration, + required this.amount, + required this.location, + required this.description, + required this.details, + required this.createdDate, + required this.categoryName, + required this.subcategoryName, + required this.video, + required this.images1, + }); + + factory VendorServiceModel.fromJson(Map json) { + return VendorServiceModel( + id: json['id'] ?? 0, + vendorId: json['vendor_id'] ?? 0, + serviceType: json['service_type'] ?? 0, + vendorName: json['vendorname'] ?? '', + serviceName: json['servicename'] ?? '', + category: json['category'] ?? 0, + subcategory: json['subcategory'] ?? 0, + workingHours: json['workinghours'] ?? '', + workingDuration: json['workingduration'] ?? '', + amount: json['amount']?.toString() ?? '', + location: json['location'] ?? '', + description: json['description'] ?? '', + details: json['details'] ?? '', + createdDate: json['created_date'] ?? '', + categoryName: json['category_name'] ?? '', + subcategoryName: json['subcategory_name'] ?? '', + video: _parseStringList(json['videos']), + images1: _parseStringList(json['images1']), + ); + } + + Map toJson() { + return { + 'id': id, + 'vendor_id': vendorId, + 'service_type': serviceType, + 'vendorname': vendorName, + 'servicename': serviceName, + 'category': category, + 'subcategory': subcategory, + 'workinghours': workingHours, + 'workingduration': workingDuration, + 'amount': amount, + 'location': location, + 'description': description, + 'details': details, + 'created_date': createdDate, + 'category_name': categoryName, + 'subcategory_name': subcategoryName, + 'videos': video, + 'images1': images1, + }; + } + + static List _parseStringList(dynamic input) { + if (input == null) return []; + + if (input is List) { + return input.map((e) => e.toString()).toList(); + } + + if (input is String) { + if (input.trim().isEmpty) return []; + + if (input.trim().startsWith('[')) { + try { + final parsed = jsonDecode(input); + if (parsed is List) { + return parsed.map((e) => e.toString()).toList(); + } + } catch (_) {} + } + + if (input.contains(',')) { + return input + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + } + + return [input]; + } + + return []; + } +} diff --git a/lib/model/vendor_model/vendor_serviceupload_model.dart b/lib/model/vendor_model/vendor_serviceupload_model.dart new file mode 100644 index 0000000..59ec2be --- /dev/null +++ b/lib/model/vendor_model/vendor_serviceupload_model.dart @@ -0,0 +1,74 @@ +class VendorServiceUploadModel { + final String id; + final String vendorName; + final String serviceName; + final String serviceType; + final String category; + final String subcategory; + final String workingHours; + final String workingDuration; + final double amount; + final String location; + final String? description; + final String details; + final List images; // This will store image URLs after upload + final List videos; // YouTube video links + + VendorServiceUploadModel({ + required this.id, + required this.vendorName, + required this.serviceName, + required this.serviceType, + required this.category, + required this.subcategory, + required this.workingHours, + required this.workingDuration, + required this.amount, + required this.location, + this.description, + required this.details, + required this.images, + required this.videos, + }); + + Map toJson() { + return { + 'id': id, + 'vendorName': vendorName, + 'serviceName': serviceName, + 'serviceType': serviceType, + 'category': category, + 'subcategory': subcategory, + 'workingHours': workingHours, + 'workingDuration': workingDuration, + 'amount': amount, + 'location': location, + 'description': description, + 'details': details, + 'images': images, + 'videos': videos, + }; + } + + factory VendorServiceUploadModel.fromJson(Map json) { + return VendorServiceUploadModel( + id: json['id'] as String, + vendorName: json['vendorName'] as String, + serviceName: json['serviceName'] as String, + serviceType: json['serviceType'] as String, + category: json['category'] as String, + subcategory: json['subcategory'] as String, + workingHours: json['workingHours'] as String, + workingDuration: json['workingDuration'] as String, + amount: + json['amount'] is int + ? (json['amount'] as int).toDouble() + : json['amount'] as double, + location: json['location'] as String, + description: json['description'] as String?, + details: json['details'] as String, + images: (json['images'] as List?)?.map((e) => e as String).toList() ?? [], + videos: (json['videos'] as List?)?.map((e) => e as String).toList() ?? [], + ); + } +} diff --git a/lib/model/vendor_model/vendorregister_model.dart b/lib/model/vendor_model/vendorregister_model.dart new file mode 100644 index 0000000..08c9279 --- /dev/null +++ b/lib/model/vendor_model/vendorregister_model.dart @@ -0,0 +1,38 @@ +class VendorRegisterModel { + String name; + String number; + String email; + String? location; + String? pincode; + String? aadharCard; + String? panCard; + String? drivingLicense; + String? passport; + String userId; + + VendorRegisterModel({ + required this.name, + required this.number, + required this.email, + this.location, + this.pincode, + this.aadharCard, + this.panCard, + this.drivingLicense, + this.passport, + this.userId = '', + }); + + Map toJson() => { + 'name': name, + 'number': number, + 'email': email, + 'location': location, + 'pincode': pincode, + 'aadhar_card': aadharCard, + 'pan_card': panCard, + 'driving_license': drivingLicense, + 'passport': passport, + 'user_id': userId, + }; +} diff --git a/lib/routers/consts_router.dart b/lib/routers/consts_router.dart new file mode 100644 index 0000000..f2331ce --- /dev/null +++ b/lib/routers/consts_router.dart @@ -0,0 +1,33 @@ +class RouterConts { + static const String flashScreen = '/flash'; + static const String welcomepage = '/welcomepage'; + static const String loginpage = '/loginpage'; + static const String siguppage = '/siguppage'; + static const String otp = '/otp'; + static const String main = '/main'; + static const String homescreen = '/homescreen'; + static const String packageList = '/packages'; + static const String history = '/history'; + static const String listservice = '/listservice'; + static const String profilemainscreen = '/profilemainscreen'; + static const String editprofile = '/editprofile'; + static const String categorypage = '/categorypage'; + static const String detailserivce = '/detailserivce'; + static const String bookingserivce = '/bookingserivce'; + static const String changepassword = '/changepassword'; + //comman + static const String notification = '/notification'; + //--------------------vendor flow------------------------- + static const String vendorwelcome = '/vendorwelcome'; + static const String vendorresgister = '/vendorresgister'; + static const String vendorhomepage = '/vendorhomepage'; + static const String vendorpackage = '/vendorpackage'; + static const String vendorcategory = '/vendorcategory'; + static const String venodor = '/main'; + static const String vendorhistory = '/vendorhistory'; + static const String vendorservice = '/vendorservice'; + static const String vendorserviceupload = '/vendorserviceupload'; + static const String profilemainvendor = '/profilemainvendor'; + static const String vendoreditprofile = '/vendoreditprofile'; + static const String mostpopluarserviceviewall = '/mostpopluarserviceviewall'; +} diff --git a/lib/routers/router.dart b/lib/routers/router.dart new file mode 100644 index 0000000..d6e29f5 --- /dev/null +++ b/lib/routers/router.dart @@ -0,0 +1,247 @@ +import 'package:bookmywages/model/detail_page_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_service_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/auth/login_page.dart'; +import 'package:bookmywages/view/auth/otp_screen.dart'; +import 'package:bookmywages/view/auth/sign_up_screen.dart'; +import 'package:bookmywages/view/intro_screens/flash_screen.dart'; +import 'package:bookmywages/view/intro_screens/vendor_welcome_page.dart'; +import 'package:bookmywages/view/intro_screens/welcome_page.dart'; +import 'package:bookmywages/view/user_main_screens/Category_page.dart'; +import 'package:bookmywages/view/user_main_screens/booking_screen.dart'; +import 'package:bookmywages/view/user_main_screens/detail_service_page.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/history_main_contoller.dart'; +import 'package:bookmywages/view/user_main_screens/home_screen.dart'; +import 'package:bookmywages/view/user_main_screens/list_service_screen.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:bookmywages/view/user_main_screens/most_popluar_viewall.dart'; +import 'package:bookmywages/view/user_main_screens/notification_page.dart'; +import 'package:bookmywages/view/user_main_screens/package_screen.dart'; +import 'package:bookmywages/view/user_main_screens/profile_screens/edit_profile.dart'; +import 'package:bookmywages/view/user_main_screens/profile_screens/profile_changepass.dart'; +import 'package:bookmywages/view/user_main_screens/profile_screens/profile_main_page.dart'; +import 'package:bookmywages/view/vendor_main_screens/Vendor_profile/vendor_edit_profile.dart'; +import 'package:bookmywages/view/vendor_main_screens/Vendor_profile/vendor_profile_main.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_catgories.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_history_maincontroller.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_service.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_serviceupload.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_homepage.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_package_page.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_register_page.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppRoutes { + + static final routes = [ + GetPage(name: RouterConts.flashScreen, page: () => const FlashScreen()), + GetPage(name: RouterConts.welcomepage, page: () => const WelcomePage()), + GetPage(name: RouterConts.loginpage, page: () => const LoginScreen()), + GetPage(name: RouterConts.siguppage, page: () => const SignUpScreen()), + GetPage( + name: RouterConts.otp, + page: () => OtpScreen(userId: Get.arguments), + ), + + // 🎯 Wrap main screens inside MainController + GetPage( + name: RouterConts.main, + page: () => MainController(child: HomeScreen()), + ), + GetPage( + name: RouterConts.homescreen, + page: () => MainController(child: HomeScreen()), + ), + + + GetPage( + name: RouterConts.listservice, + page: () { + final arguments = Get.arguments ?? {}; + final id = arguments['id']?.toString() ?? '0'; + final subcategoryId = arguments['subcategoryId']; + final service = arguments['service']; + final sourceTab = arguments['sourceTab'] ?? 0; + + return MainController( + initialBottomIndex: sourceTab, + child: ListServiceScreen( + id: id, + subcategoryId: subcategoryId, + service: service, + ), + ); + }, + ), + GetPage( + name: RouterConts.detailserivce, + page: () { + final arguments = Get.arguments ?? {}; + + // Extract and convert ID to int + int id = 0; // Default value + int sourceTab = 0; + + if (arguments is String) { + id = int.tryParse(arguments) ?? 0; + } else if (arguments is Map) { + id = arguments['id'] is int + ? arguments['id'] + : int.tryParse(arguments['id']?.toString() ?? '') ?? 0; + sourceTab = arguments['sourceTab'] ?? 0; + } else if (arguments is int) { + id = arguments; + } + + return MainController( + initialBottomIndex: sourceTab, + child: DetailServicePage(id: id), // Now passing int + ); + }, + ), + GetPage( + name: RouterConts.bookingserivce, + page: () { + // Get the service model from arguments + final arguments = Get.arguments ?? {}; + final DetailPageModel service = arguments is DetailPageModel + ? arguments + : arguments['service'] as DetailPageModel; + + // 🎯 NEW: Get source tab for booking screen + final sourceTab = arguments is Map ? arguments['sourceTab'] ?? 0 : 0; + + return MainController( + initialBottomIndex: sourceTab, // 🎯 Maintain tab state + child: BookingScreen(service: service), + ); + }, + ), + GetPage( + name: RouterConts.mostpopluarserviceviewall, + page: () => MainController( + initialBottomIndex: 0, // 🎯 Always from home + child: MostPopluarViewall(), + ), + ), + GetPage( + name: RouterConts.profilemainscreen, + page: () => MainController(child: ProfileMainPage()), + ), + GetPage( + name: RouterConts.editprofile, + page: () => MainController(child: EditProfile()), + ), + GetPage( + name: RouterConts.changepassword, + page: () => MainController(child: ProfileChangepass()), + ), + GetPage( + name: RouterConts.packageList, + page: () => MainController(child: PackageScreen()), + ), + GetPage( + name: RouterConts.categorypage, + page: () => MainController(child: CategoryPage()), + ), + GetPage( + name: RouterConts.history, + page: () { + final arguments = Get.arguments; + int? historyTabIndex; + + // Handle different argument types + if (arguments is Map) { + // Get history tab index from map + historyTabIndex = arguments['historyTab']; + } else if (arguments is int) { + // For backward compatibility - treat as history tab index + historyTabIndex = arguments; + } + + return MainController( + child: HistoryScreen(initialTabIndex: historyTabIndex), + initialBottomIndex: 3, // Always set bottom nav to history (index 3) + ); + }, + ), + GetPage(name: RouterConts.vendorwelcome, page: () => VendorWelcomePage()), + GetPage( + name: RouterConts.vendorresgister, + page: () => VendorRegisterPage(), + ), + GetPage( + name: RouterConts.venodor, + page: () => VendorController(child: VendorHomepage()), + ), + + GetPage( + name: RouterConts.vendorhomepage, + page: () => VendorController(child: VendorHomepage()), + ), + GetPage( + name: RouterConts.profilemainvendor, + page: () => VendorController(child: VendorProfileMain()), + ), + GetPage( + name: RouterConts.vendoreditprofile, + page: () => VendorController(child: VendorEditProfile()), + ), + GetPage( + name: RouterConts.vendorpackage, + page: () => VendorController(child: VendorPackagePage()), + ), + GetPage( + name: RouterConts.vendorcategory, + page: () => VendorController(child: VendorCatgories()), + ), + GetPage( + name: RouterConts.vendorhistory, + page: () { + final arguments = Get.arguments; + int? historyTabIndex; + + // Handle different argument types + if (arguments is Map) { + // Get history tab index from map + historyTabIndex = arguments['historyTab']; + } else if (arguments is int) { + // For backward compatibility - treat as history tab index + historyTabIndex = arguments; + } + + return VendorController( + child: VendorHistoryMainController( + initialTabIndex: historyTabIndex, // Pass the tab index + ), + ); + }, + ), + GetPage( + name: RouterConts.vendorservice, + page: () { + final args = Get.arguments as Map; + return VendorController( + child: VendorService(id: args['id'], tittle: args['tittle']), + ); + }, + ), + GetPage( + name: RouterConts.vendorserviceupload, + page: () { + final VendorServiceModel service = Get.arguments as VendorServiceModel; + + return VendorController(child: VendorServiceupload(service: service)); + }, + ), + + ]; + + + + +} + diff --git a/lib/view/auth/auth_repository.dart b/lib/view/auth/auth_repository.dart new file mode 100644 index 0000000..f9da76d --- /dev/null +++ b/lib/view/auth/auth_repository.dart @@ -0,0 +1,1870 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:bookmywages/model/Banner_model.dart'; +import 'package:bookmywages/model/Booking_model.dart'; +import 'package:bookmywages/model/Categories_model.dart'; +import 'package:bookmywages/model/detail_page_model.dart'; +import 'package:bookmywages/model/enquriy_list_model.dart'; +import 'package:bookmywages/model/enquriy_model.dart'; +import 'package:bookmywages/model/expired_plan_model.dart'; +import 'package:bookmywages/model/get_review_model.dart'; +import 'package:bookmywages/model/login_model.dart'; +import 'package:bookmywages/model/most_popular_model.dart'; +import 'package:bookmywages/model/notification_model.dart'; +import 'package:bookmywages/model/otp_model.dart'; +import 'package:bookmywages/model/package_model.dart'; +import 'package:bookmywages/model/payment_details_model.dart'; +import 'package:bookmywages/model/profile_get_model.dart'; +import 'package:bookmywages/model/service_model.dart'; +import 'package:bookmywages/model/subcategory_model.dart'; +import 'package:bookmywages/model/user_booking_details.dart' + show UserBookingDetails; +import 'package:bookmywages/model/vendor_model/terms_and_conditions_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_booking_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_catgories_model.dart' + show VendorCategoriesModel; +import 'package:bookmywages/model/vendor_model/vendor_profile_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_service_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_serviceupload_model.dart'; +import 'package:bookmywages/model/vendor_model/vendorregister_model.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http_parser/http_parser.dart'; // Add this at the top + +class AuthRepository { + Future> loginUser( + String url, + LoginModel loginModel, + ) async { + try { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(loginModel.toJson()), + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + print('Decoded Response: $decoded'); // Debug print + + dynamic data = decoded['data']; + String userId = ''; + String isverify = ''; + + // Handle different response structures + if (data is List && data.isNotEmpty) { + // Response contains a list of data + userId = data[0]['id']?.toString() ?? ''; + isverify = data[0]['is_verified']?.toString() ?? ''; + } else if (data is Map) { + // Response contains a single data object + userId = data['id']?.toString() ?? ''; + isverify = data['is_verified']?.toString() ?? ''; + + // Check if vendor_id exists in the data map + } else { + throw Exception('Unexpected response structure: $data'); + } + + // Store both values in SharedPreferences + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('userId', userId); + await prefs.setString('is_verified', isverify); + + // Also store the full data JSON string for other operations + await prefs.setString('data', userId); + + return decoded; + } else { + throw Exception( + 'Login failed with status code ${response.statusCode}: ${response.body}', + ); + } + } catch (e) { + print('Login error: $e'); + throw Exception('Login failed: $e'); + } + } + + Future> registerUser( + String url, + Map data, + ) async { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(data), + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + print('Decoded Response: $decoded'); // Debug print + + dynamic data = decoded['data']; + + String userId; + String isverify; + + if (data is List && data.isNotEmpty) { + userId = data[0]['id'].toString(); + isverify = data[0]['is_verified'].toString(); + } else if (data is Map && data.containsKey('id')) { + userId = data['id'].toString(); + isverify = data['is_verified'].toString(); + } else { + throw Exception('Unexpected response structure: $data'); + } + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('userId', userId); + await prefs.setString('is_verified', isverify); + + return decoded; + } else { + throw Exception('Register failed: ${response.body}'); + } + } + + // Add to AuthRepository class + Future> verifyOtp(String url, OtpModel otpModel) async { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(otpModel.toJson()), + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + print('OTP Verify Response: $decoded'); + + // Directly store is_verified = '1' + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('is_verified', '1'); + + return decoded; + } else { + throw Exception('OTP verification failed: ${response.body}'); + } + } +} + +class BannerRepository { + Future> fetchBanners(String url) async { + try { + final response = await http + .get(Uri.parse(url)) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List bannerList = jsonBody['data'] ?? []; + return bannerList.map((json) => BannerModel.fromJson(json)).toList(); + } else { + return []; // return empty list on server error + } + } catch (_) { + return []; // return empty list on timeout or any error + } + } +} + +class CategoryRepository { + Future> fetchCategories(String url) async { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List categoryList = jsonBody['data'] ?? []; + + return categoryList + .map((json) => CategoriesModel.fromJson(json)) + .toList(); + } else { + throw Exception('Failed to load categories'); + } + } +} + +class packageRepository { + Future> fetchpackage(String url) async { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List packagelist = jsonBody['data'] ?? []; + + return packagelist.map((json) => PackageModel.fromJson(json)).toList(); + } else { + throw Exception('Failed to load categories'); + } + } +} + +class SubcategoryRepository { + Future> fetchSubcategories( + String url, + String categoryId, + ) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'userId': userId, 'category': categoryId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => SubcategoryModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load subcategories'); + } + } +} + +class ProfilegetRepository { + Future> fetchprofileget(String url) async { + // Retrieve userId from SharedPreferences + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + // Debugging: Print the userId to make sure it's correct + print('User ID: $userId'); + + // Check if userId is available before making the request + if (userId.isEmpty) { + throw Exception('User ID is not available.'); + } + + // Send the userId in the API request + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + // Check if the response status code is successful + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + + // Convert the JSON response to a list of ProfileGetModel objects + return list.map((e) => ProfileGetModel.fromJson(e)).toList(); + } else { + // Handle API errors + throw Exception( + 'Failed to load profile data, status code: ${response.statusCode}', + ); + } + } +} + +class ServiceRepository { + Future> fetchService( + String url, + String categoryId, [ + String? subcategoryId, + String? selecttype, // Optional parameter in square brackets + ]) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + // Create the request body + Map requestBody = { + 'user_id': userId, + 'category': categoryId, + }; + + // Add subcategory only if it's provided + if (subcategoryId != null && subcategoryId.isNotEmpty) { + requestBody['subcategory'] = subcategoryId; + } + if (selecttype != null && selecttype.isNotEmpty) { + requestBody['service_type'] = selecttype; + } + print('Request Body: $requestBody'); // Debug log + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(requestBody), + ); + + if (response.statusCode == 200) { + print('Response: ${response.body}'); // Debug log + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => ServiceModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load services: ${response.statusCode}'); + } + } +} + +class ProfileupdateRepository { + Future> fetchprofileupdate(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) throw Exception('User ID is not available.'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => ProfileGetModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load profile, code: ${response.statusCode}'); + } + } + + Future updateProfile({ + required String url, + required String name, + required String number, + required String email, + required String address, + File? imageFile, + }) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final request = http.MultipartRequest('POST', Uri.parse(url)); + request.fields['user_id'] = userId; + request.fields['name'] = name; + request.fields['number'] = number; + request.fields['email'] = email; + request.fields['address'] = address; + + if (imageFile != null) { + request.files.add( + await http.MultipartFile.fromPath( + 'profile_pic', // Make sure this matches your backend key + imageFile.path, + contentType: MediaType('image', 'jpeg'), // Optional but recommended + ), + ); + } + + final response = await request.send(); + + if (response.statusCode != 200) { + throw Exception('Failed to update profile'); + } + } +} + +class GetReviewRepository { + Future> fetchgetreview( + String url, + String serviceId, + ) async { + Map requestBody = {'service_id': serviceId}; + print('Request Body: $requestBody'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(requestBody), + ); + + if (response.statusCode == 200) { + print('Response: ${response.body}'); + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => GetReviewModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load reviews: ${response.statusCode}'); + } + } +} + +class MostPopularRepository { + Future> fetchMostPopular( + String url, + String categoryId, + ) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + Map requestBody = { + 'user_id': userId, + 'category': categoryId, + }; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(requestBody), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => MostPopularModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load services: ${response.statusCode}'); + } + } +} + +class EnquriyRepository { + Future> fetchenquriy(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) throw Exception('User ID is not available.'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => EnquiryModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load profile, code: ${response.statusCode}'); + } + } + + Future updateEnquriy({ + required BuildContext context, + required String url, + required String name, + required String number, + required String email, + required String message, + required String serviceid, + }) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final request = http.MultipartRequest('POST', Uri.parse(url)); + request.fields['user_id'] = userId; + request.fields['name'] = name; + request.fields['mobile'] = number; + request.fields['email'] = email; + request.fields['message'] = message; + request.fields['service_id'] = serviceid; + + final response = await request.send(); + if (response.statusCode == 200) { + Fluttertoast.showToast( + msg: "Enquiry Successful", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } else { + Fluttertoast.showToast( + msg: "Failed to update profile", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + throw Exception('Failed to update profile'); + } + } +} + +class BookingRepository { + Future> fetchBooking(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) throw Exception('User ID is not available.'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => BookingModel.fromJson(e)).toList(); + } else { + throw Exception( + 'Failed to load booking data. Code: ${response.statusCode}', + ); + } + } + + Future updateBooking({ + required BuildContext context, + required String url, + required String name, + required String number, + required String email, + required String message, + required String address, + required String date, + required String time, + required String serviceId, + }) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) { + Fluttertoast.showToast( + msg: "User ID not found", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + return; + } + + final request = http.MultipartRequest('POST', Uri.parse(url)); + request.fields['user_id'] = userId; + request.fields['name'] = name; + request.fields['mobile_number'] = number; + request.fields['email'] = email; + request.fields['message'] = message; + request.fields['service_id'] = serviceId; + request.fields['address'] = address; + request.fields['service_date'] = date; + request.fields['service_time'] = time; + + final response = await request.send(); + final responseBody = await response.stream.bytesToString(); + final jsonResponse = jsonDecode(responseBody); + + if (response.statusCode == 200) { + Fluttertoast.showToast( + msg: "Booking Successful", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } else if (response.statusCode == 404 && + jsonResponse['data'] == 'You can book maximum 1 services.') { + Fluttertoast.showToast( + msg: jsonResponse['data'], + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } else { + Fluttertoast.showToast( + msg: + jsonResponse['data'] ?? + 'Failed to submit enquiry. Code: ${response.statusCode}', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + throw Exception('Failed to update booking'); + } + } +} + +// ignore: camel_case_types +class updatereviewRepository { + Future> updatereview(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) throw Exception('User ID is not available.'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => GetReviewModel.fromJson(e)).toList(); + } else { + throw Exception( + 'Failed to load booking data. Code: ${response.statusCode}', + ); + } + } + + Future updatereviews({ + required BuildContext context, + required String url, + required String review, + required String serviceId, + }) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) { + Fluttertoast.showToast( + msg: "User ID not found", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + return; + } + + final request = http.MultipartRequest('POST', Uri.parse(url)); + request.fields['user_id'] = userId; + request.fields['review'] = review; + + request.fields['service_id'] = serviceId; + + final response = await request.send(); + final responseBody = await response.stream.bytesToString(); + final jsonResponse = jsonDecode(responseBody); + + if (response.statusCode == 200) { + Fluttertoast.showToast( + msg: "Review submitted successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + } else { + Fluttertoast.showToast( + msg: + jsonResponse['data'] ?? + 'Failed to submit review. Code: ${response.statusCode}', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + throw Exception('Failed to update review'); + } + } +} + +class enquriylistRepository { + Future> fetchenquirylist(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + if (userId.isEmpty) throw Exception('User ID is not available.'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => EnquiryListModel.fromJson(e)).toList(); + } else { + throw Exception( + 'Failed to load booking data. Code: ${response.statusCode}', + ); + } + } +} + +class EnquriydeleteRepository { + Future enquriydelete(String url, String id) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId, 'id': id}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + // Assuming your API returns a success flag or message + // Adjust this based on your actual API response + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to delete enquiry'); + } + } +} + +class PaymentdetailsRepository { + Future> fetchpayment(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + + final List dataList = jsonBody['data'] ?? []; + return dataList + .map((json) => PaymentDetailsModel.fromJson(json)) + .toList(); + } else { + throw Exception('Failed to fetch payment details'); + } + } +} + +class PaymentdeleteRepository { + Future paymentdelete(String url, String id) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId, 'id': id}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + // Assuming your API returns a success flag or message + // Adjust this based on your actual API response + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to delete payment Details'); + } + } +} + +class ChangepasswordRepository { + Future changepassword(String url, String password) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId, 'password': password}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + // Assuming your API returns a success flag or message + // Adjust this based on your actual API response + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to changepassword'); + } + } +} + +class UserhistoryBookingdetailsRepository { + Future> fetchhistorybooking(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + print(response.body); + print(jsonBody); + + final List dataList = jsonBody['data'] ?? []; + return dataList.map((json) => UserBookingDetails.fromJson(json)).toList(); + } else { + throw Exception('Failed to fetch payment details'); + } + } +} + +class CancelbookingRepository { + Future cancelbooking({ + required String url, + required String id, + required String serviceId, + required String type, + }) async { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'id': id, 'service_id': serviceId, 'type': type}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to cancel booking'); + } + } +} + +class BookingmodifyRepository { + Future bookingmodify({ + required String url, + required String id, + required String serviceId, + required String servicedate, + required String servicetime, + }) async { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'id': id, + 'service_id': serviceId, + 'service_date': servicedate, + 'service_time': servicetime, + }), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to modify'); + } + } +} + +class ExpiredPlanRepository { + Future fetchExpiredPlan(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final Map data = jsonBody['data'] ?? {}; + return ExpiredPlanModel.fromJson(data); + } else { + throw Exception('Failed to fetch expired plan'); + } + } +} + +class PlanSuccessRepository { + Future planSuccess({ + required String url, + required String userId, + required String planId, + required int duration, + required int type, + }) async { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'user_id': userId, + 'plan_id': planId, + 'duration': duration.toString(), + 'type': type.toString(), + }), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to subscribe to plan'); + } + } +} + +////vendor flow +class GetvendorId { + Future fetchGetid(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final data = jsonBody['data']; + + // Extract and store vendor_id and vendor_status if not null + if (data != null) { + final vendorId = data['vendor_id']?.toString() ?? ''; + final vendorStatus = data['vendor_status']?.toString() ?? ''; + + await prefs.setString('vendor_id', vendorId); + await prefs.setString('vendor_status', vendorStatus); + + print('Stored vendor_id: $vendorId'); + print('Stored vendor_status: $vendorStatus'); + } + + return data; + } else { + throw Exception('Failed to fetch vendor data'); + } + } +} + +class VendorRegister { + Future> vendorRegister( + String url, + VendorRegisterModel vendorModel, + ) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + vendorModel.userId = userId; // inject userId into the model + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(vendorModel.toJson()), + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + print('Decoded Response: $decoded'); + return decoded; + } else { + throw Exception('Register failed: ${response.body}'); + } + } +} + +class VendorGetProfile { + Future> vendorGetProfile(String url) async { + try { + final prefs = await SharedPreferences.getInstance(); + final dataId = prefs.getString('vendor_id') ?? ''; + + if (dataId.isEmpty) { + throw Exception('Data ID not found in SharedPreferences.'); + } + + final body = jsonEncode({'dataId': dataId}); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: body, + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + print('Decoded Response: $decoded'); + return decoded; // Return the decoded response directly + } else { + throw Exception( + 'Get profile failed with status ${response.statusCode}: ${response.body}', + ); + } + } catch (e) { + print('Error in vendorGetProfile: $e'); + rethrow; + } + } +} + +class Vendorserviceupload { + Future> vendorserviceupload( + String url, + VendorServiceUploadModel serviceupload, + List images, + ) async { + try { + // Create a multipart request for uploading files and data + var request = http.MultipartRequest('POST', Uri.parse(url)); + + // Add service data as fields + request.fields['vendor_id'] = serviceupload.id; + request.fields['vendorname'] = serviceupload.vendorName; + request.fields['servicename'] = serviceupload.serviceName; + request.fields['service_type'] = serviceupload.serviceType; + request.fields['category'] = serviceupload.category; + request.fields['subcategory'] = serviceupload.subcategory; + request.fields['workinghours'] = serviceupload.workingHours; + request.fields['workingduration'] = serviceupload.workingDuration; + request.fields['amount'] = serviceupload.amount.toString(); + request.fields['location'] = serviceupload.location; + request.fields['description'] = serviceupload.description ?? ''; + request.fields['details'] = serviceupload.details; + + // Add video links as JSON array + if (serviceupload.videos.isNotEmpty) { + request.fields['videos'] = jsonEncode(serviceupload.videos); + } + + // Add images as files with array notation + for (int i = 0; i < images.length; i++) { + final file = images[i]; + final fileName = + 'image_${i}_${DateTime.now().millisecondsSinceEpoch}.jpg'; + + // Get file extension from path + String extension = file.path.split('.').last.toLowerCase(); + String mimeType = 'image/jpeg'; // Default + + // Set correct mime type based on extension + if (extension == 'png') { + mimeType = 'image/png'; + } else if (extension == 'jpg' || extension == 'jpeg') { + mimeType = 'image/jpeg'; + } else if (extension == 'gif') { + mimeType = 'image/gif'; + } else if (extension == 'webp') { + mimeType = 'image/webp'; + } + + // Add the file to the request with indexed array notation + request.files.add( + http.MultipartFile( + 'images[$i]', // Indexed array notation for multiple images + file.readAsBytes().asStream(), + file.lengthSync(), + filename: fileName, + contentType: MediaType.parse(mimeType), + ), + ); + } + + // Send the request + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + + // Parse response regardless of status code + try { + final decoded = jsonDecode(response.body) as Map; + print('Decoded Response: $decoded'); + + // Check if request was successful + if (response.statusCode == 200 || response.statusCode == 201) { + return decoded; + } else { + // For failed requests, extract error message + String errorMessage = + decoded['data']?.toString() ?? + decoded['message']?.toString() ?? + 'Upload failed with status: ${response.statusCode}'; + + // Return the error response for handling upstream + return { + 'success': false, + 'error': errorMessage, + 'statusCode': response.statusCode, + 'data': decoded, + }; + } + } catch (jsonError) { + // If JSON parsing fails, use raw response + String errorMessage = + 'Request failed with status: ${response.statusCode}. Response: ${response.body}'; + + Fluttertoast.showToast( + msg: 'Server response error', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + + throw Exception(errorMessage); + } + } on SocketException catch (e) { + String errorMessage = 'No internet connection: ${e.message}'; + Fluttertoast.showToast( + msg: 'No internet connection', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + throw Exception(errorMessage); + } on http.ClientException catch (e) { + String errorMessage = 'Network error: ${e.message}'; + Fluttertoast.showToast( + msg: 'Network error occurred', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + throw Exception(errorMessage); + } on FormatException catch (e) { + String errorMessage = 'Invalid response format: ${e.message}'; + Fluttertoast.showToast( + msg: 'Invalid server response', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + throw Exception(errorMessage); + } catch (e) { + String errorMessage = 'Unexpected error: $e'; + Fluttertoast.showToast( + msg: 'Something went wrong', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + throw Exception(errorMessage); + } + } +} + +class VendorbookingdetailsRepository { + Future> fetchvendorbooking(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + print(response.body); + print(jsonBody); + + final List dataList = jsonBody['data'] ?? []; + return dataList.map((json) => VendorBookingModel.fromJson(json)).toList(); + } else { + throw Exception('Failed to fetch booking details'); + } + } +} + +class ChangebookingstatusRepository { + Future changebooking({ + required String url, + required String id, + required String status, + }) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'id': id, 'user_id': userId, 'status': status}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to update booking status'); + } + } +} + +class VendorcatgoriesRepository { + Future> fetchvendorcat(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + + final List dataList = jsonBody['data'] ?? []; + return dataList + .map((json) => VendorCategoriesModel.fromJson(json)) + .toList(); + } else { + throw Exception('Failed to fetch booking details'); + } + } +} + +class VendorserviceRepository { + Future> fetchvendorservice(String url) async { + try { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + if (userId.isEmpty) { + throw Exception('Vendor ID not found in SharedPreferences'); + } + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + + // Add more detailed logging + + if (jsonBody['data'] == null) { + return []; + } + + final List dataList = jsonBody['data'] ?? []; + return dataList + .map((json) => VendorServiceModel.fromJson(json)) + .toList(); + } else { + throw Exception( + 'Failed to fetch service details: ${response.statusCode}', + ); + } + } catch (e) { + throw Exception('Failed to fetch service details: $e'); + } + } +} + +// Updated API Service Class +class Vendorserviceupadate { + Future> vendorserviceupdate( + String url, + VendorServiceUploadModel serviceupload, + List newImages, + ) async { + try { + var request = http.MultipartRequest('POST', Uri.parse(url)); + + // Add all basic fields + request.fields.addAll({ + 'id': serviceupload.id, + 'vendorname': serviceupload.vendorName, + 'servicename': serviceupload.serviceName, + 'service_type': serviceupload.serviceType, + 'category': serviceupload.category, + 'subcategory': serviceupload.subcategory, + 'workinghours': serviceupload.workingHours, + 'workingduration': serviceupload.workingDuration, + 'amount': serviceupload.amount.toString(), + 'location': serviceupload.location, + 'description': serviceupload.description ?? '', + 'details': serviceupload.details, + }); + + // Handle videos - send as JSON string + if (serviceupload.videos.isNotEmpty) { + request.fields['videos'] = jsonEncode(serviceupload.videos); + print('Videos JSON: ${request.fields['videos']}'); + } + + // Process ALL images (existing + new) as multipart files + List allImageFiles = []; + + // Step 1: Convert existing images (URLs) to files if needed + if (serviceupload.images.isNotEmpty) { + print('Processing ${serviceupload.images.length} existing images...'); + + for (int i = 0; i < serviceupload.images.length; i++) { + final imageUrl = serviceupload.images[i]; + print('Processing existing image $i: $imageUrl'); + + try { + // If it's already a local file path + if (imageUrl.startsWith('/') || imageUrl.startsWith('file://')) { + final file = File(imageUrl.replaceFirst('file://', '')); + if (await file.exists()) { + allImageFiles.add(file); + } else { + print('⚠ Existing file not found: ${file.path}'); + } + } + // If it's a URL, download it + else if (imageUrl.startsWith('http')) { + final downloadedFile = await _downloadImageAsFile(imageUrl, i); + if (downloadedFile != null) { + allImageFiles.add(downloadedFile); + print( + '✓ Downloaded and added existing image: ${downloadedFile.path}', + ); + } else { + print('⚠ Failed to download existing image: $imageUrl'); + } + } + // If it's a relative path or asset + else { + print('⚠ Unsupported existing image format: $imageUrl'); + } + } catch (e) { + print('⚠ Error processing existing image $imageUrl: $e'); + } + } + } + + // Step 2: Add new images to the list + allImageFiles.addAll(newImages); + + print('Total images to upload: ${allImageFiles.length}'); + + // Step 3: Upload all images with same field name 'images[]' + if (allImageFiles.isNotEmpty) { + print('Processing ${allImageFiles.length} total images for upload...'); + + for (int i = 0; i < allImageFiles.length; i++) { + final file = allImageFiles[i]; + + // Validate file exists + if (!await file.exists()) { + print('WARNING: File does not exist: ${file.path}'); + continue; + } + + // Get file info + final fileSize = await file.length(); + final fileName = file.path.split('/').last; + final extension = fileName.split('.').last.toLowerCase(); + + print('Processing file $i: $fileName ($fileSize bytes)'); + + // Validate file size (5MB limit) + if (fileSize > 5 * 1024 * 1024) { + continue; + } + + // Determine MIME type + String mimeType; + switch (extension) { + case 'png': + mimeType = 'image/png'; + break; + case 'jpg': + case 'jpeg': + mimeType = 'image/jpeg'; + break; + case 'gif': + mimeType = 'image/gif'; + break; + case 'webp': + mimeType = 'image/webp'; + break; + default: + mimeType = 'image/jpeg'; + } + + // Create unique filename for upload + final uploadFileName = + 'upload_${DateTime.now().millisecondsSinceEpoch}_$i.$extension'; + + try { + // Method 1: Try fromPath (preferred) + var multipartFile = await http.MultipartFile.fromPath( + 'images[]', // All images use same field name + file.path, + filename: uploadFileName, + contentType: MediaType.parse(mimeType), + ); + + request.files.add(multipartFile); + } catch (fromPathError) { + // Method 2: Fallback to fromBytes + try { + final bytes = await file.readAsBytes(); + var multipartFile = http.MultipartFile.fromBytes( + 'images[]', + bytes, + filename: uploadFileName, + contentType: MediaType.parse(mimeType), + ); + + request.files.add(multipartFile); + // ignore: empty_catches + } catch (fromBytesError) {} + } + } + } else { + print('No images to upload'); + } + + // Add headers + request.headers.addAll({ + 'Accept': 'application/json', + 'Content-Type': 'multipart/form-data', + }); + + // Debug request info + print('=== REQUEST SUMMARY ==='); + print('Fields: ${request.fields.keys.toList()}'); + print('Files: ${request.files.length}'); + for (var file in request.files) { + print( + ' - Field: ${file.field}, Filename: ${file.filename}, Length: ${file.length}', + ); + } + + // Send request with timeout + print('Sending request...'); + var streamedResponse = await request.send().timeout( + Duration(seconds: 120), // 2 minute timeout for large uploads + onTimeout: () { + throw TimeoutException('Upload timeout after 2 minutes'); + }, + ); + + var response = await http.Response.fromStream(streamedResponse); + + print('=== RESPONSE INFO ==='); + print('Status Code: ${response.statusCode}'); + print('Response Headers: ${response.headers}'); + print('Response Body Length: ${response.body.length}'); + print( + 'Response Body Preview: ${response.body.length > 500 ? "${response.body.substring(0, 500)}..." : response.body}', + ); + + // Handle response + if (response.statusCode >= 200 && response.statusCode < 300) { + try { + final decoded = jsonDecode(response.body) as Map; + print('✓ Successfully parsed JSON response'); + return decoded; + } catch (jsonError) { + print('⚠ JSON parsing failed: $jsonError'); + // Return success with raw response if JSON parsing fails but status is OK + return { + 'success': true, + 'message': 'Update completed successfully', + 'raw_response': response.body, + }; + } + } else { + // Handle error response + String errorMessage; + try { + final errorResponse = jsonDecode(response.body); + errorMessage = + errorResponse['message'] ?? + errorResponse['error'] ?? + 'Server returned status ${response.statusCode}'; + } catch (e) { + errorMessage = + 'Server error (${response.statusCode}): ${response.body}'; + } + + print('✗ Server error: $errorMessage'); + throw Exception(errorMessage); + } + } on TimeoutException catch (e) { + print('✗ Timeout error: $e'); + throw Exception( + 'Upload timeout. Please check your connection and try again.', + ); + } on SocketException catch (e) { + print('✗ Network error: $e'); + throw Exception('Network error. Please check your internet connection.'); + } on http.ClientException catch (e) { + print('✗ HTTP client error: $e'); + throw Exception('Connection error: ${e.message}'); + } catch (e) { + print('✗ Unexpected error: $e'); + rethrow; + } + } + + // Helper method to download existing images from URLs + Future _downloadImageAsFile(String imageUrl, int index) async { + try { + final response = await http.get(Uri.parse(imageUrl)); + + if (response.statusCode == 200) { + // Get app's temporary directory + final directory = await getTemporaryDirectory(); + + // Determine file extension from URL or content type + String extension = 'jpg'; + if (imageUrl.contains('.png')) { + extension = 'png'; + } else if (imageUrl.contains('.gif')) + extension = 'gif'; + else if (imageUrl.contains('.webp')) + extension = 'webp'; + else if (response.headers['content-type']?.contains('png') == true) + extension = 'png'; + + // Create temporary file + final fileName = + 'existing_image_${index}_${DateTime.now().millisecondsSinceEpoch}.$extension'; + final file = File('${directory.path}/$fileName'); + + // Write downloaded bytes to file + await file.writeAsBytes(response.bodyBytes); + + return file; + } else { + print('Failed to download image: ${response.statusCode}'); + return null; + } + } catch (e) { + print('Error downloading image $imageUrl: $e'); + return null; + } + } +} + +// Image Upload Helper Class +class ImageUploadHelper { + static Future> pickMultipleImages() async { + try { + final picker = ImagePicker(); + final List pickedFiles = await picker.pickMultiImage( + maxWidth: 1920, + maxHeight: 1920, + imageQuality: 85, + ); + + List validFiles = []; + + for (XFile xFile in pickedFiles) { + File file = File(xFile.path); + + if (await validateImageFile(file)) { + validFiles.add(file); + } else { + print('Invalid image file: ${xFile.name}'); + } + } + + return validFiles; + } catch (e) { + print('Error picking images: $e'); + return []; + } + } + + static Future validateImageFile(File file) async { + try { + if (!await file.exists()) { + print('File does not exist: ${file.path}'); + return false; + } + + // Check file size (max 5MB) + int fileSize = await file.length(); + if (fileSize > 5 * 1024 * 1024) { + print('File too large: ${file.path} ($fileSize bytes)'); + return false; + } + + // Check file extension + String extension = file.path.split('.').last.toLowerCase(); + List validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + if (!validExtensions.contains(extension)) { + print('Invalid file extension: $extension'); + return false; + } + + return true; + } catch (e) { + print('Error validating file: $e'); + return false; + } + } +} + +class VendorExpiredPlanRepository { + Future fetchvendorExpiredPlan(String url) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'user_id': userId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final Map data = jsonBody['data'] ?? {}; + return ExpiredPlanModel.fromJson(data); + } else { + throw Exception('Failed to fetch expired plan'); + } + } +} + +class DetailpageRepository { + Future> fetchDetailpage( + String url, + int serviceid, + ) async { + final response = await http.get( + Uri.parse('$url?service_id=$serviceid'), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final data = jsonBody['data']; + + // Handle single object or list + if (data is List) { + return data.map((e) => DetailPageModel.fromJson(e)).toList(); + } else if (data is Map) { + return [DetailPageModel.fromJson(data)]; + } else { + return []; + } + } else { + throw Exception('Failed to load detail page'); + } + } +} + +class CatManagementDeleteRepository { + Future deleteCategoryManagement(String url, String categoryId) async { + final prefs = await SharedPreferences.getInstance(); + final vendorId = prefs.getString('vendor_id') ?? ''; + + try { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': vendorId, 'category': categoryId}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to delete category: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Network error: $e'); + } + } +} + +class SerciceDeleteRepository { + Future deleteService(String url, String id) async { + final prefs = await SharedPreferences.getInstance(); + final vendorId = prefs.getString('vendor_id') ?? ''; + + try { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': vendorId, 'id': id}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return jsonBody['success'] == true || + jsonBody['status'] == 'success' || + jsonBody['message']?.toLowerCase().contains('success') == true; + } else { + throw Exception('Failed to delete service: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Network error: $e'); + } + } +} + +class ProfilegetvendorRepository { + Future> fetchprofilegetvendor(String url) async { + // Retrieve userId from SharedPreferences + final prefs = await SharedPreferences.getInstance(); + final vendorid = prefs.getString('vendor_id') ?? ''; + + // Debugging: Print the userId to make sure it's correct + print('vendor_id: $vendorid'); + + // Check if userId is available before making the request + if (vendorid.isEmpty) { + throw Exception('User ID is not available.'); + } + + // Send the userId in the API request + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': vendorid}), + ); + + // Check if the response status code is successful + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + + // Convert the JSON response to a list of ProfileGetModel objects + return list.map((e) => ProfileGetModel.fromJson(e)).toList(); + } else { + // Handle API errors + throw Exception( + 'Failed to load profile data, status code: ${response.statusCode}', + ); + } + } +} + +class ProfileupdatevendorRepository { + Future> fetchProfileupdatevendor(String url) async { + final prefs = await SharedPreferences.getInstance(); + final vendorid = prefs.getString('vendor_id') ?? ''; + + if (vendorid.isEmpty) throw Exception('User ID is not available.'); + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'vendor_id': vendorid}), + ); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + final List list = jsonBody['data'] ?? []; + return list.map((e) => VendorProfileModel.fromJson(e)).toList(); + } else { + throw Exception('Failed to load profile, code: ${response.statusCode}'); + } + } + + Future updateProfilevendor({ + required String url, + required String name, + required String number, + required String email, + required String address, + File? imageFile, + }) async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + final request = http.MultipartRequest('POST', Uri.parse(url)); + request.fields['vendor_id'] = userId; + request.fields['name'] = name; + request.fields['number'] = number; + request.fields['email'] = email; + request.fields['address'] = address; + + if (imageFile != null) { + request.files.add( + await http.MultipartFile.fromPath( + 'profile_pic', // Make sure this matches your backend key + imageFile.path, + contentType: MediaType('image', 'jpeg'), // Optional but recommended + ), + ); + } + + final response = await request.send(); + + if (response.statusCode != 200) { + throw Exception('Failed to update profile'); + } + } +} + +class TermsAndConditionsRepository { + Future fetchTermsAndCondition(String url) async { + try { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return TermsAndConditionsModel.fromJson(jsonBody); + } else { + throw Exception( + 'Failed to load terms and conditions, status code: ${response.statusCode}', + ); + } + } catch (e) { + throw Exception('Failed to fetch terms and conditions: $e'); + } + } +} + +class PrivacypolicyRepository { + Future fetchPrivacypolicy(String url) async { + try { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + return TermsAndConditionsModel.fromJson(jsonBody); + } else { + throw Exception( + 'Failed to load terms and conditions, status code: ${response.statusCode}', + ); + } + } catch (e) { + throw Exception('Failed to fetch terms and conditions: $e'); + } + } +} + +class NotificationRepository { + Future> fetchNotification({ + required int type, + required String userId, + }) async { + try { + final uri = Uri.parse(ConstsApi.notification); + + final response = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'type': type, 'user_id': userId}), + ); + + print('Fetching notification from: $uri'); + print('Status Code: ${response.statusCode}'); + print('Response Body: ${response.body}'); + + if (response.statusCode == 200) { + final jsonBody = jsonDecode(response.body); + + // Handle if data is inside "data" key + if (jsonBody is Map && jsonBody['data'] is List) { + final List dataList = jsonBody['data']; + return dataList + .map((e) => NotificationModel.fromJson(e)) + .toList(); + } + + // Handle if directly a List + if (jsonBody is List) { + return jsonBody + .map((e) => NotificationModel.fromJson(e)) + .toList(); + } + + return []; // no recognizable data + } else { + throw Exception('Status Code: ${response.statusCode}'); + } + } catch (e) { + print('Error fetching notification: $e'); + throw Exception('Failed to fetch notification: $e'); + } + } +} diff --git a/lib/view/auth/login_page.dart b/lib/view/auth/login_page.dart new file mode 100644 index 0000000..0c1580b --- /dev/null +++ b/lib/view/auth/login_page.dart @@ -0,0 +1,301 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/model/login_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + bool _obscurePassword = true; + + // Add form key and error messages + final _formKey = GlobalKey(); + String? _emailError; + String? _passwordError; + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + // Validation methods + void _validateEmail(String value) { + if (value.isEmpty) { + setState(() => _emailError = 'Email is required'); + } else if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + setState(() => _emailError = 'Enter a valid email'); + } else { + setState(() => _emailError = null); + } + } + + void _validatePassword(String value) { + if (value.isEmpty) { + setState(() => _passwordError = 'Password is required'); + } else if (value.length < 4) { + setState(() => _passwordError = 'Password must be at least 4 characters'); + } else { + setState(() => _passwordError = null); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + final double horizontalPadding = screenSize.width * 0.05; + final double imageHeight = screenSize.height * 0.45; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Column( + children: [ + // Image + Image.asset( + AppAssets.login, + width: screenSize.width, + height: imageHeight, + fit: BoxFit.cover, + ), + + // Overlapping Login Container using Transform + Transform.translate( + offset: const Offset(0, -100), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.73), + border: Border.all( + width: 1.09, + color: const Color(0xFFA39898), + ), + boxShadow: const [ + BoxShadow( + offset: Offset(0, -4), + blurRadius: 12.3, + color: Color(0x66E0E0E0), + ), + BoxShadow( + offset: Offset(0, 4), + blurRadius: 4, + color: Color(0x66000000), + ), + ], + color: Colors.white, + ), + child: Padding( + padding: EdgeInsets.all(screenSize.width * 0.05), + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 20), + const Text( + "Sign in", + style: TextStyle( + fontSize: 32, + fontFamily: "Gilroy-ExtraBold", + fontWeight: FontWeight.w800, + height: 1.0, + letterSpacing: 0.64, + ), + ), + SizedBox(height: screenSize.height * 0.03), + CommonTextFormField( + hintText: 'Enter your email', + controller: _usernameController, + prefixIcon: const Icon( + Icons.person, + color: AppColors.hittext, + ), + errorText: _emailError, + onChanged: (value) { + _validateEmail(value); + }, + ), + const SizedBox(height: 42), + CommonTextFormField( + hintText: 'Enter your password', + controller: _passwordController, + obscureText: _obscurePassword, + prefixIcon: const Icon( + Icons.lock, + color: AppColors.hittext, + ), + errorText: _passwordError, + onChanged: (value) { + _validatePassword(value); + }, + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + color: AppColors.hittext, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: const Text( + "Forgot Password?", + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14, + height: 1.0, + letterSpacing: 0.1357, + color: Color(0xFFFF0000), + ), + ), + ), + ), + const SizedBox(height: 20), + CommanButton( + text: 'Sign in', + textStyle: const TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w800, + fontSize: 20, + height: 1.0, + color: AppColors.secondprimary, + ), + onPressed: () { + // Validate all fields + _validateEmail(_usernameController.text); + _validatePassword(_passwordController.text); + + if (_emailError == null && + _passwordError == null) { + final loginModel = LoginModel( + email: _usernameController.text.trim(), + password: _passwordController.text.trim(), + ); + + ref + .read( + loginFutureProvider(loginModel).future, + ) + .then((response) async { + Fluttertoast.showToast( + msg: + 'Login Successful: ${response['message']}', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + fontSize: 10.0, + ); + final userId = response['data'][0]['id']; + final prefs = + await SharedPreferences.getInstance(); + final isVerified = prefs.getString( + 'is_verified', + ); + + if (isVerified == '1') { + Get.toNamed(RouterConts.homescreen); + } else { + Get.toNamed( + RouterConts.otp, + arguments: userId, + ); // Replace with your OTP route + } + }) + .catchError((error) { + Fluttertoast.showToast( + msg: 'Login Failed: $error', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 10.0, + ); + }); + } else { + Fluttertoast.showToast( + msg: 'Please fix all errors', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.orange, + textColor: Colors.white, + fontSize: 10.0, + ); + } + }, + + isPrimary: true, + backgroundColor: AppColors.primary, + textColor: AppColors.secondprimary, + width: 230, + ), + SizedBox(height: 30), + RichText( + text: TextSpan( + text: "Didn't have account? ", + style: TextStyle( + fontFamily: 'Gilroy-ExtraBold', + fontWeight: FontWeight.w700, + fontSize: 17, + height: 1.0, + letterSpacing: 0.01, + color: Color(0xFF9F9F9F), + ), + children: [ + TextSpan( + text: "Sign up", + style: TextStyle(color: Color(0xFF0066FF)), + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.toNamed(RouterConts.siguppage); + }, + ), + ], + ), + ), + SizedBox(height: 30), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/auth/otp_screen.dart b/lib/view/auth/otp_screen.dart new file mode 100644 index 0000000..2f8d295 --- /dev/null +++ b/lib/view/auth/otp_screen.dart @@ -0,0 +1,300 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/model/otp_model.dart' show OtpModel; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +import 'package:pinput/pinput.dart'; + +class OtpScreen extends ConsumerStatefulWidget { + final String? userId; + + const OtpScreen({super.key, required this.userId}); + + @override + ConsumerState createState() => _OtpScreenState(); +} + +class _OtpScreenState extends ConsumerState { + String _enteredOtp = ''; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + // No need to parse userId, keeping it as String + } + + void _verifyOtp() async { + if (widget.userId!.isEmpty || _enteredOtp.length != 4) { + Fluttertoast.showToast( + msg: "Invalid OTP or missing user information", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // Use userId directly as String + final otpModel = OtpModel( + userId: widget.userId.toString(), + otp: _enteredOtp, + ); + + final result = await ref.read(otpVerificationProvider(otpModel).future); + + if (mounted) { + Fluttertoast.showToast( + msg: "Verification Successful: ${result['message'] ?? 'Success'}", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + // context.push(RouterConts.homescreen); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: "Verification Failed: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + void _resendOtp() async { + if (widget.userId!.isEmpty) { + Fluttertoast.showToast( + msg: "Missing user information", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // Implement resend OTP API call using String userId + // For example: + // final result = await ref.read(resendOtpProvider(widget.userId).future); + + if (mounted) { + Fluttertoast.showToast( + msg: "OTP resent successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: "Failed to resend OTP: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final double horizontalPadding = screenSize.width * 0.05; + + return WillPopScope( + onWillPop: () async { + // context.push(RouterConts.siguppage); + return false; + }, + child: Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: true, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + Image.asset( + AppAssets.login, + width: screenSize.width, + fit: BoxFit.cover, + ), + Transform.translate( + offset: const Offset(0, -150), + child: Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: EdgeInsets.all(screenSize.width * 0.05), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.73), + border: Border.all( + width: 1.09, + color: const Color(0xFFA39898), + ), + boxShadow: const [ + BoxShadow( + offset: Offset(0, -4), + blurRadius: 12.3, + color: Color(0x66E0E0E0), + ), + BoxShadow( + offset: Offset(0, 4), + blurRadius: 4, + color: Color(0x66000000), + ), + ], + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 20), + const Text( + "OTP Verification", + style: TextStyle( + fontSize: 28, + fontFamily: "Gilroy-ExtraBold", + fontWeight: FontWeight.w800, + height: 1.0, + letterSpacing: 0.64, + ), + ), + SizedBox(height: screenSize.height * 0.03), + const Text( + "Enter the OTP sent to your number", + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 16, + height: 1.0, + letterSpacing: 0.16, + ), + ), + const SizedBox(height: 27), + Pinput( + length: 4, + defaultPinTheme: PinTheme( + width: 56, + height: 56, + textStyle: const TextStyle( + fontSize: 20, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey, + width: 3, + ), + ), + ), + ), + focusedPinTheme: PinTheme( + width: 56, + height: 56, + textStyle: const TextStyle( + fontSize: 20, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.primary, + width: 3, + ), + ), + ), + ), + onChanged: (pin) => setState(() => _enteredOtp = pin), + onCompleted: + (pin) => setState(() => _enteredOtp = pin), + ), + const SizedBox(height: 40), + RichText( + text: TextSpan( + text: "Didn't receive code? ", + style: const TextStyle( + fontFamily: 'Gilroy-ExtraBold', + fontWeight: FontWeight.w700, + fontSize: 17, + height: 1.0, + letterSpacing: 0.01, + color: Color(0xFF9F9F9F), + ), + children: [ + TextSpan( + text: "Resend", + style: const TextStyle( + color: Color(0xFF0066FF), + ), + recognizer: + TapGestureRecognizer()..onTap = _resendOtp, + ), + ], + ), + ), + const SizedBox(height: 40), + CommanButton( + text: 'Verify', + textStyle: const TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w800, + fontSize: 20, + height: 1.0, + color: AppColors.secondprimary, + ), + onPressed: _verifyOtp, + isPrimary: true, + backgroundColor: AppColors.primary, + textColor: AppColors.secondprimary, + width: 230, + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/auth/sign_up_screen.dart b/lib/view/auth/sign_up_screen.dart new file mode 100644 index 0000000..a176021 --- /dev/null +++ b/lib/view/auth/sign_up_screen.dart @@ -0,0 +1,223 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +class SignUpScreen extends ConsumerStatefulWidget { + const SignUpScreen({super.key}); + + @override + ConsumerState createState() => _SignUpScreenState(); +} + +class _SignUpScreenState extends ConsumerState { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _mobilenumberController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + bool _obscurePassword = true; + + @override + void dispose() { + _usernameController.dispose(); + _mobilenumberController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + void _onSignUp() async { + final userData = { + 'name': _usernameController.text.trim(), + 'number': _mobilenumberController.text.trim(), + 'email': _emailController.text.trim(), + 'password': _passwordController.text.trim(), + }; + + try { + final result = await ref.read(signupFutureProvider(userData).future); + + final userId = result['data'][0]['id']; + + if (mounted) { + Fluttertoast.showToast( + msg: "Signup Successful: ${result['message'] ?? 'Success'}", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + + Get.toNamed(RouterConts.otp, arguments: userId); + + // Pass userId to OTP screen + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: "Signup Failed: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final double horizontalPadding = screenSize.width * 0.05; + + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () async { + // context.push(RouterConts.welcomepage); // Actually go back + return false; // Prevent default behavior (optional) + }, + + child: Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: true, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + Image.asset( + AppAssets.login, + width: screenSize.width, + fit: BoxFit.cover, + ), + Transform.translate( + offset: const Offset(0, -150), + child: Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: EdgeInsets.all(screenSize.width * 0.05), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.73), + border: Border.all( + width: 1.09, + color: const Color(0xFFA39898), + ), + boxShadow: const [ + BoxShadow( + offset: Offset(0, -4), + blurRadius: 12.3, + color: Color(0x66E0E0E0), + ), + BoxShadow( + offset: Offset(0, 4), + blurRadius: 4, + color: Color(0x66000000), + ), + ], + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Sign up", + style: TextStyle( + fontSize: 32, + fontFamily: "Gilroy-ExtraBold", + fontWeight: FontWeight.w800, + height: 1.0, + letterSpacing: 0.64, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + hintText: 'Enter your username', + controller: _usernameController, + prefixIcon: const Icon( + Icons.person, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 16), + + CommonTextFormField( + hintText: 'Enter your mobile number', + controller: _mobilenumberController, + prefixIcon: const Icon( + Icons.phone, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 16), + + CommonTextFormField( + hintText: 'Enter your email', + controller: _emailController, + prefixIcon: const Icon( + Icons.email, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 16), + + CommonTextFormField( + hintText: 'Enter your password', + controller: _passwordController, + obscureText: _obscurePassword, + prefixIcon: const Icon( + Icons.lock, + color: AppColors.hittext, + ), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + color: AppColors.hittext, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + + const SizedBox(height: 15), + + CommanButton( + text: 'Sign up', + textStyle: const TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w800, + fontSize: 20, + height: 1.0, + color: AppColors.secondprimary, + ), + onPressed: _onSignUp, + isPrimary: true, + backgroundColor: AppColors.primary, + textColor: AppColors.secondprimary, + width: 230, + ), + + const SizedBox(height: 10), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/intro_screens/flash_screen.dart b/lib/view/intro_screens/flash_screen.dart new file mode 100644 index 0000000..b656f23 --- /dev/null +++ b/lib/view/intro_screens/flash_screen.dart @@ -0,0 +1,49 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class FlashScreen extends StatefulWidget { + const FlashScreen({super.key}); + + @override + State createState() => _FlashScreenState(); +} + +class _FlashScreenState extends State { + @override + void initState() { + super.initState(); + _navigateUser(); + } + + Future _navigateUser() async { + await Future.delayed(const Duration(seconds: 2)); + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId'); + final isVerified = prefs.getString('is_verified'); + + if (!mounted) return; + + if (userId != null && userId.isNotEmpty && isVerified == '1') { + Get.toNamed(RouterConts.homescreen); + } else { + Get.toNamed(RouterConts.welcomepage); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.flashscreen, + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Center(child: Image.asset(AppAssets.flashscreen)), + ), + ); + } +} diff --git a/lib/view/intro_screens/vendor_welcome_page.dart b/lib/view/intro_screens/vendor_welcome_page.dart new file mode 100644 index 0000000..05cc513 --- /dev/null +++ b/lib/view/intro_screens/vendor_welcome_page.dart @@ -0,0 +1,204 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class VendorWelcomePage extends ConsumerStatefulWidget { + const VendorWelcomePage({super.key}); + + @override + ConsumerState createState() => _VendorWelcomePageState(); +} + +class _VendorWelcomePageState extends ConsumerState { + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _checkProfileStatus(); + } + + Future _checkProfileStatus() async { + try { + setState(() => _isLoading = true); + + final prefs = await SharedPreferences.getInstance(); + final vendorId = prefs.getString('vendor_id'); + final vendorStatus = prefs.getString('vendor_status'); + + print('Stored vendor_id: $vendorId'); + print('Stored vendor_status: $vendorStatus'); + + await Future.delayed(const Duration(seconds: 2)); + + if (!mounted) return; + + if (vendorId == null || vendorId.isEmpty) { + // Case 1: vendor_id is null or empty + Get.offNamed(RouterConts.vendorresgister); + } else if (vendorStatus == '0') { + // Case 2: vendor_id present but vendor_status is null + _showVerificationDialog(context); + } else { + // Case 3: both are present + Get.offNamed(RouterConts.vendorhomepage); + } + } catch (e) { + // ignore: avoid_print + print('Error checking profile status: $e'); + if (mounted) { + _showVerificationDialog(context); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + backgroundColor: AppColors.secondprimary, + body: LayoutBuilder( + builder: (context, constraints) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 33.0, + top: 48, + right: 59, + ), + child: Align( + alignment: Alignment.topLeft, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Welcome back to ", + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 32, + height: 1.5, + letterSpacing: 0.26, + ), + ), + TextSpan( + text: "VENDOR LOGIN", + style: TextStyle( + fontFamily: 'Gilroy-ExtraBold', + fontWeight: FontWeight.w800, + fontSize: 32, + height: 1.5, + letterSpacing: 0.26, + color: Color(0xFF3A47C9), + ), + ), + ], + ), + ), + ), + ), + Expanded( + child: Center( + child: Image.asset( + AppAssets.vendorwelcome, + fit: BoxFit.contain, + width: constraints.maxWidth * 0.8, + ), + ), + ), + ], + ); + }, + ), + ), + ); + } + + void _showVerificationDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, // Prevent closing by tapping outside + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + side: const BorderSide(color: Colors.white, width: 1), + ), + elevation: 10, + backgroundColor: Colors.white, + child: Container( + width: 365, + height: 305, + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Verification', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 32, + height: 1.0, + letterSpacing: 0.32, + ), + ), + const SizedBox(height: 20), + const Text( + 'Still your account will not verify by Admin, please wait with us.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20.23, + height: 1.74, + letterSpacing: 0.2023, + ), + ), + const SizedBox(height: 30), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(29.36), + ), + padding: const EdgeInsets.symmetric( + horizontal: 50, + vertical: 15, + ), + ), + onPressed: () { + Navigator.of(context).pop(); // Close dialog + Get.offNamed(RouterConts.homescreen); + }, + child: const Text( + 'OK', + style: TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w400, + fontSize: 20.29, + height: 1.0, + letterSpacing: 0.4058, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/view/intro_screens/welcome_page.dart b/lib/view/intro_screens/welcome_page.dart new file mode 100644 index 0000000..e01dc46 --- /dev/null +++ b/lib/view/intro_screens/welcome_page.dart @@ -0,0 +1,154 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; + +class WelcomePage extends StatefulWidget { + const WelcomePage({super.key}); + + @override + State createState() => _WelcomePageState(); +} + +class _WelcomePageState extends State { + @override + Widget build(BuildContext context) { + final Size screenSize = MediaQuery.of(context).size; + final double textScale = MediaQuery.of(context).textScaleFactor; + + final bool isMobile = screenSize.width < 600; + final bool isTablet = screenSize.width >= 600 && screenSize.width < 1200; + final bool isDesktop = screenSize.width >= 1200; + + final double headingFontSize = + (isMobile ? 25 : (isTablet ? 28 : 32)) * textScale; + final double buttonFontSize = + (isMobile ? 18 : (isTablet ? 22 : 25)) * textScale; + final double horizontalPadding = screenSize.width * 0.05; + final double topPadding = screenSize.height * 0.05; + final double verticalSpacing = screenSize.height * 0.03; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: SingleChildScrollView( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding.clamp(16.0, 32.0), + vertical: topPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 250), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: TextStyle( + fontFamily: 'Gilroy', + fontWeight: FontWeight.w900, + fontSize: headingFontSize, + height: 1.65, + letterSpacing: headingFontSize * 0.01, + ), + children: const [ + TextSpan( + text: "Explore The All Types Of ", + style: TextStyle(color: AppColors.thridprimary), + ), + TextSpan( + text: "Service", + style: TextStyle(color: Color(0xFF3A47C9)), + ), + ], + ), + ), + ), + SizedBox(height: verticalSpacing), + Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop + ? screenSize.width * 0.5 + : (isTablet + ? screenSize.width * 0.7 + : screenSize.width * 0.85), + ), + child: Image.asset( + AppAssets.welcome, + fit: BoxFit.contain, + ), + ), + ), + SizedBox(height: verticalSpacing * 1.6), + Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop + ? screenSize.width * 0.3 + : (isTablet + ? screenSize.width * 0.5 + : screenSize.width * 0.85), + ), + child: CommanButton( + text: 'Sign in', + textStyle: TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w800, + fontSize: buttonFontSize, + height: 1.0, + letterSpacing: buttonFontSize * 0.02, + color: AppColors.secondprimary, + ), + + onPressed: () => Get.toNamed(RouterConts.loginpage), + isPrimary: true, + backgroundColor: AppColors.primary, + textColor: AppColors.secondprimary, + ), + ), + ), + SizedBox(height: verticalSpacing), + Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop + ? screenSize.width * 0.3 + : (isTablet + ? screenSize.width * 0.5 + : screenSize.width * 0.85), + ), + child: CommanButton( + text: 'Sign up', + textStyle: TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w800, + fontSize: buttonFontSize, + height: 1.0, + letterSpacing: buttonFontSize * 0.02, + color: AppColors.hittext, + ), + + onPressed: () => Get.toNamed(RouterConts.siguppage), + isPrimary: false, + ), + ), + ), + SizedBox(height: verticalSpacing), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/user_main_screens/Category_page.dart b/lib/view/user_main_screens/Category_page.dart new file mode 100644 index 0000000..e1382e4 --- /dev/null +++ b/lib/view/user_main_screens/Category_page.dart @@ -0,0 +1,350 @@ +// ignore: file_names +import 'dart:ui'; + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; + +class CategoryPage extends ConsumerStatefulWidget { + const CategoryPage({super.key}); + + @override + ConsumerState createState() => _CategoryPageState(); +} + +class _CategoryPageState extends ConsumerState { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _handleBackNavigation() { + // Use post-frame callback to avoid accessing scaffold geometry during build + WidgetsBinding.instance.addPostFrameCallback((_) { + try { + final indexController = InheritedIndexController.of(context); + indexController?.changeIndex(0); + } catch (e) { + // Handle any potential errors gracefully + debugPrint('Error resetting index: $e'); + } + }); + } + + @override + Widget build(BuildContext context) { + final categoryAsyncValue = ref.watch(categoryListProvider); + + return PopScope( + canPop: true, + onPopInvoked: (didPop) { + if (didPop) { + _handleBackNavigation(); + } + }, + child: Scaffold( + backgroundColor: AppColors.secondprimary, + body: categoryAsyncValue.when( + data: (categories) => _buildSuccessUI(categories), + loading: () => _buildLoadingUI(), + error: (error, _) => _buildErrorUI(error), + ), + ), + ); + } + + Widget _buildSuccessUI(List categories) { + return CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + // Custom App Bar + SliverToBoxAdapter(child: _buildAppBar()), + + // Optimized Grid + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 196 / 209, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index >= categories.length) return null; + return _buildCategoryCard(categories[index]); + }, + childCount: categories.length, + addAutomaticKeepAlives: false, + addRepaintBoundaries: true, + ), + ), + ), + + // Bottom padding + const SliverToBoxAdapter(child: SizedBox(height: 20)), + ], + ); + } + + Widget _buildAppBar() { + return Container( + height: 90, + padding: const EdgeInsets.only(top: 50), + child: Stack( + alignment: Alignment.center, + children: [ + const Center( + child: Text( + "Category", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + ), + ], + ), + ); + } + + Widget _buildCategoryCard(dynamic category) { + return RepaintBoundary( + child: GestureDetector( + onTap: () { + // 🎯 UPDATED: Pass source tab as 2 (Categories tab index) + Get.toNamed( + RouterConts.listservice, + arguments: { + 'id': category.id, + 'subcategoryId': null, + 'service': '0', + 'sourceTab': 2, // 🎯 Categories tab is index 2 + }, + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Stack( + fit: StackFit.expand, + children: [ + _buildCategoryImage(category.imageUrl), + _buildImageOverlay(category.name), + ], + ), + ), + ), + ), + ); + } + + Widget _buildCategoryImage(String imageUrl) { + return Image.network( + imageUrl, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: Colors.grey[300], + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) => Container( + color: Colors.grey[300], + child: const Center( + child: Icon(Icons.image_not_supported, color: Colors.grey, size: 32), + ), + ), + cacheWidth: 400, + cacheHeight: 400, + ); + } + + Widget _buildImageOverlay(String categoryName) { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + height: 56, + width: 196, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(255, 255, 255, 0.54), + Color.fromRGBO(153, 153, 153, 0.54), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Center( + child: Text( + categoryName, + style: const TextStyle( + color: Colors.black87, + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.2, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildLoadingUI() { + return CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter(child: _buildAppBar()), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 196 / 209, + ), + delegate: SliverChildBuilderDelegate( + (context, index) => _buildShimmerCard(), + childCount: 6, + ), + ), + ), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey[300], + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + Colors.grey[300]!, + Colors.grey[100]!, + Colors.grey[300]!, + ], + ), + ), + ), + Positioned( + bottom: 12, + left: 12, + right: 12, + child: Container( + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.grey[400], + ), + ), + ), + ], + ), + ); + } + + Widget _buildErrorUI(Object error) { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: _buildAppBar()), + SliverFillRemaining( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'Oops! Something went wrong', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + 'Please check your connection and try again', + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + // Use post-frame callback for state changes + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.invalidate(categoryListProvider); + }); + }, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/view/user_main_screens/booking_screen.dart b/lib/view/user_main_screens/booking_screen.dart new file mode 100644 index 0000000..c994465 --- /dev/null +++ b/lib/view/user_main_screens/booking_screen.dart @@ -0,0 +1,1338 @@ +import 'dart:convert'; + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/model/detail_page_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/auth/auth_repository.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:bookmywages/view/user_main_screens/sucessfull_screen.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dotted_line/dotted_line.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class BookingScreen extends StatefulWidget { + final DetailPageModel service; + + const BookingScreen({super.key, required this.service}); + + @override + State createState() => _BookingScreenState(); +} + +class _BookingScreenState extends State { + final _formKey = GlobalKey(); + int rating = 0; + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _mobileController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _addressController = TextEditingController(); + final TextEditingController _messageController = TextEditingController(); + + // Controllers for date and time + final TextEditingController _dateController = TextEditingController(); + final TextEditingController _timeController = TextEditingController(); + + // Variables to store selected date and time + DateTime? _selectedDate; + TimeOfDay? _selectedTime; + + // ScrollController to manage scrolling when keyboard appears + final ScrollController _scrollController = ScrollController(); + + // FocusNodes for each field - SOLUTION 1: Use FocusNodes + final FocusNode _nameFocusNode = FocusNode(); + final FocusNode _mobileFocusNode = FocusNode(); + final FocusNode _emailFocusNode = FocusNode(); + final FocusNode _addressFocusNode = FocusNode(); + final FocusNode _messageFocusNode = FocusNode(); + + // SOLUTION 2: Debounce validation to prevent frequent rebuilds + bool _isValidating = false; + + @override + void initState() { + super.initState(); + // SOLUTION 3: Remove listeners that cause frequent setState calls + // Only add listeners if absolutely necessary + _nameController.addListener(_onTextChangedDebounced); + _mobileController.addListener(_onTextChangedDebounced); + _emailController.addListener(_onTextChangedDebounced); + _addressController.addListener(_onTextChangedDebounced); + _messageController.addListener(_onTextChangedDebounced); + _dateController.addListener(_onTextChangedDebounced); + _timeController.addListener(_onTextChangedDebounced); + } + + // SOLUTION 4: Debounced validation to prevent frequent rebuilds + void _onTextChangedDebounced() { + if (_isValidating) return; + + _isValidating = true; + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted && _formKey.currentState != null) { + // Only validate if form has been submitted before + // This prevents validation during typing + _isValidating = false; + } + }); + } + + @override + void dispose() { + // Remove listeners before disposing controllers + _nameController.removeListener(_onTextChangedDebounced); + _mobileController.removeListener(_onTextChangedDebounced); + _emailController.removeListener(_onTextChangedDebounced); + _addressController.removeListener(_onTextChangedDebounced); + _messageController.removeListener(_onTextChangedDebounced); + _dateController.removeListener(_onTextChangedDebounced); + _timeController.removeListener(_onTextChangedDebounced); + + // Dispose FocusNodes + _nameFocusNode.dispose(); + _mobileFocusNode.dispose(); + _emailFocusNode.dispose(); + _addressFocusNode.dispose(); + _messageFocusNode.dispose(); + + _nameController.dispose(); + _mobileController.dispose(); + _emailController.dispose(); + _addressController.dispose(); + _messageController.dispose(); + _dateController.dispose(); + _timeController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + // Function to open date picker + Future _selectDate(BuildContext context) async { + final DateTime now = DateTime.now(); + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? now, + firstDate: now, + lastDate: DateTime(now.year + 1, now.month, now.day), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: Colors.blue, + onPrimary: Colors.white, + onSurface: Colors.black, + ), + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + _dateController.text = DateFormat('dd-MM-yyyy').format(picked); + }); + } + } + + // Function to open time picker + Future _selectTime(BuildContext context) async { + final TimeOfDay now = TimeOfDay.now(); + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime ?? now, + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: Colors.blue, + onPrimary: Colors.white, + onSurface: Colors.black, + ), + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedTime) { + setState(() { + _selectedTime = picked; + final hour = picked.hourOfPeriod == 0 ? 12 : picked.hourOfPeriod; + final period = picked.period == DayPeriod.am ? 'AM' : 'PM'; + _timeController.text = + '${hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')} $period'; + }); + } + } + + @override + Widget build(BuildContext context) { + final service = widget.service; + final screenWidth = MediaQuery.of(context).size.width; + final indexController = InheritedIndexController.of(context); + return Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + // Top app bar + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 10.0, left: 16, right: 16), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () => Navigator.of(context).pop(), + ), + Expanded( + child: Center( + child: Text( + 'Booking', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + const SizedBox(width: 48), + ], + ), + ), + ), + ), + + // Main scrollable content + Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 56, + ), + child: ListView( + controller: _scrollController, + physics: const ClampingScrollPhysics(), + children: [ + // Image + Overlay + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: const EdgeInsets.all(0.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: service.images1.isNotEmpty + ? CachedNetworkImage( + imageUrl: service.images1.first, + width: double.infinity, + height: 300, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + const Icon(Icons.error), + ) + : Container( + width: double.infinity, + height: 300, + color: Colors.grey[300], + child: const Center( + child: Icon( + Icons.image_not_supported, + size: 50, + ), + ), + ), + ), + ), + Positioned( + bottom: -130, + left: 16, + right: 16, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFCFCFC), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: const Color(0xFFC9C9C9), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + service.vendorName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + letterSpacing: 0.2, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 5), + Text( + service.servicename, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w600, + fontSize: 16, + letterSpacing: 0.18, + color: Color(0xFF5A5A5A), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 15), + const DottedLine( + dashColor: Color(0xFFBABABA), + lineThickness: 1, + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Payment:", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + ), + ), + const SizedBox(height: 15), + Text( + 'Rs ${service.amount}', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF636363), + ), + ), + ], + ), + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Duration:", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + ), + ), + const SizedBox(height: 15), + Text( + service.workingduration, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF636363), + ), + ), + ], + ), + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Rating:", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + ), + ), + const SizedBox(height: 15), + Row( + children: [ + const Icon( + Icons.star, + size: 16, + color: Colors.amber, + ), + const SizedBox(width: 4), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 140), + + // Form + Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + // SOLUTION 5: Change autovalidate mode to only validate after user interaction + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildFieldTitle("Name"), + const SizedBox(height: 8), + buildTextFormField( + controller: _nameController, + focusNode: _nameFocusNode, // Add FocusNode + hintText: "Enter your name", + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your name'; + } + return null; + }, + onTap: () { + Future.delayed( + const Duration(milliseconds: 300), + () { + _scrollToField(0); + }, + ); + }, + ), + const SizedBox(height: 16), + buildFieldTitle("Mobile Number"), + const SizedBox(height: 8), + buildTextFormField( + controller: _mobileController, + focusNode: _mobileFocusNode, // Add FocusNode + hintText: "Enter your mobile number", + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your mobile number'; + } + if (!RegExp(r'^[0-9]{10}$').hasMatch(value)) { + return 'Please enter a valid 10-digit mobile number'; + } + return null; + }, + onTap: () { + Future.delayed( + const Duration(milliseconds: 300), + () { + _scrollToField(1); + }, + ); + }, + ), + const SizedBox(height: 16), + buildFieldTitle("Email ID"), + const SizedBox(height: 8), + buildTextFormField( + controller: _emailController, + focusNode: _emailFocusNode, // Add FocusNode + hintText: "Enter your email address", + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email address'; + } + if (!RegExp( + r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', + ).hasMatch(value)) { + return 'Please enter a valid email address'; + } + return null; + }, + onTap: () { + Future.delayed( + const Duration(milliseconds: 300), + () { + _scrollToField(2); + }, + ); + }, + ), + const SizedBox(height: 16), + + // Date and Time Row + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildFieldTitle("Date"), + const SizedBox(height: 8), + TextFormField( + controller: _dateController, + readOnly: true, + decoration: InputDecoration( + hintText: "Select Date", + filled: true, + fillColor: Colors.white, + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Color(0xFFB7B7B7), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Color(0xFFB7B7B7), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Color(0xFFB7B7B7), + width: 1, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.red, + width: 1, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.red, + width: 1, + ), + ), + suffixIcon: IconButton( + icon: const Icon( + Icons.calendar_today, + color: Colors.blue, + ), + onPressed: () => _selectDate(context), + ), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontSize: 14, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select a date'; + } + return null; + }, + onTap: () => _selectDate(context), + ), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildFieldTitle("Time"), + const SizedBox(height: 8), + TextFormField( + controller: _timeController, + readOnly: true, + decoration: InputDecoration( + hintText: "Select Time", + filled: true, + fillColor: Colors.white, + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Color(0xFFB7B7B7), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Color(0xFFB7B7B7), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Color(0xFFB7B7B7), + width: 1, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.red, + width: 1, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: Colors.red, + width: 1, + ), + ), + suffixIcon: IconButton( + icon: const Icon( + Icons.access_time, + color: Colors.blue, + ), + onPressed: () => _selectTime(context), + ), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontSize: 14, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select a time'; + } + return null; + }, + onTap: () => _selectTime(context), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + buildFieldTitle("Address"), + const SizedBox(height: 8), + buildTextFormField( + controller: _addressController, + focusNode: _addressFocusNode, // Add FocusNode + hintText: "Enter your address", + maxLines: 1, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your address'; + } + return null; + }, + onTap: () { + Future.delayed( + const Duration(milliseconds: 300), + () { + _scrollToField(3); + }, + ); + }, + ), + const SizedBox(height: 16), + buildFieldTitle("Message (Optional)"), + const SizedBox(height: 8), + // SOLUTION 6: Special handling for message field + buildTextFormField( + controller: _messageController, + focusNode: _messageFocusNode, // Add FocusNode + hintText: "Any special instructions?", + maxLines: 3, + onTap: () { + // SOLUTION 7: Ensure focus is properly managed + _messageFocusNode.requestFocus(); + Future.delayed( + const Duration(milliseconds: 300), + () { + _scrollToField(4); + }, + ); + }, + ), + const SizedBox(height: 24), + SizedBox( + width: screenWidth - 32, + height: 52, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + onPressed: () async { + // Hide keyboard + FocusScope.of(context).unfocus(); + + if (_formKey.currentState!.validate()) { + try { + final prefs = + await SharedPreferences.getInstance(); + final userId = + prefs.getString('userId') ?? + prefs.getString('user_id') ?? + ''; + + if (userId.isEmpty) { + Fluttertoast.showToast( + msg: + 'User session expired. Please login again.', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + return; + } + + if (_selectedDate == null) { + Fluttertoast.showToast( + msg: 'Please select a date', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.orange, + textColor: Colors.white, + ); + return; + } + + if (_selectedTime == null) { + Fluttertoast.showToast( + msg: 'Please select a time', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.orange, + textColor: Colors.white, + ); + return; + } + + final formattedDate = DateFormat( + 'yyyy-MM-dd', + ).format(_selectedDate!); + + final requestBody = { + 'user_id': userId, + 'name': _nameController.text.trim(), + 'mobile_number': _mobileController.text + .trim(), + 'email': _emailController.text.trim(), + 'message': _messageController.text.trim(), + 'address': _addressController.text.trim(), + 'service_date': formattedDate, + 'service_time': _timeController.text.trim(), + 'service_id': service.id.toString(), + }; + + print('API URL: ${ConstsApi.bookservice}'); + print( + 'Request Body: ${jsonEncode(requestBody)}', + ); + + if (!ConstsApi.bookservice.startsWith( + 'http', + )) { + print( + 'ERROR: Invalid API URL - must start with http/https', + ); + if (context.mounted) { + Fluttertoast.showToast( + msg: + 'Invalid API configuration. Please contact support.', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + return; + } + + print('Testing API connectivity...'); + final isConnected = + await _testApiConnectivity(); + if (!isConnected) { + if (context.mounted) { + Fluttertoast.showToast( + msg: + 'Cannot reach server. Please check your internet connection.', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.orange, + textColor: Colors.white, + ); + } + return; + } + + final response = await http + .post( + Uri.parse(ConstsApi.bookservice), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ) + .timeout(const Duration(seconds: 30)); + + print( + 'Response Status: ${response.statusCode}', + ); + print('Response Body: ${response.body}'); + print( + 'Response Headers: ${response.headers}', + ); + + final status = response.statusCode; + + if (response.headers['content-type'] + ?.contains('text/html') == + true || + response.body.trim().startsWith( + '', + ) || + response.body.trim().startsWith( + ' jsonResponse; + try { + jsonResponse = jsonDecode(response.body); + } catch (e) { + print('JSON Parse Error: $e'); + print('Raw Response: ${response.body}'); + + if (context.mounted) { + Fluttertoast.showToast( + msg: + 'Invalid response from server. Please try again.', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + return; + } + + if (status == 200) { + // Clear fields + _nameController.clear(); + _mobileController.clear(); + _emailController.clear(); + _messageController.clear(); + _addressController.clear(); + _dateController.clear(); + _timeController.clear(); + + setState(() { + _selectedDate = null; + _selectedTime = null; + }); + + if (context.mounted) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const SucessfullScreen(), + ), + ); + + int rating = 0; + await showDialog( + context: context, + barrierColor: Colors.black.withOpacity( + 0.5, + ), + builder: (context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(40), + ), + child: StatefulBuilder( + builder: (context, setState) { + return Container( + width: double.infinity, + height: 280, + padding: const EdgeInsets.all( + 16, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular( + 40, + ), + ), + child: Padding( + padding: + const EdgeInsets.symmetric( + vertical: 32, + ), + child: Column( + mainAxisSize: + MainAxisSize.min, + children: [ + const Text( + 'Rate this service', + style: TextStyle( + fontSize: 20, + fontWeight: + FontWeight.bold, + ), + ), + const SizedBox( + height: 29, + ), + RatingBar( + initialRating: 0, + minRating: 0.5, + direction: + Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + ratingWidget: RatingWidget( + full: Icon( + Icons.star, + color: AppColors + .primary, + ), + half: Icon( + Icons.star_half, + color: AppColors + .primary, + ), + empty: Icon( + Icons + .star_outline, + color: + Colors.black, + ), + ), + itemPadding: + const EdgeInsets.symmetric( + horizontal: 4.0, + ), + onRatingUpdate: + (newRating) { + setState(() { + rating = + newRating + .round(); + }); + }, + ), + const SizedBox( + height: 20, + ), + Align( + alignment: + Alignment.center, + child: SizedBox( + width: 207, + height: 47, + child: ElevatedButton( + onPressed: () async { + if (rating == + 0) { + Fluttertoast.showToast( + msg: + 'Please provide a rating', + toastLength: + Toast + .LENGTH_SHORT, + gravity: + ToastGravity + .BOTTOM, + backgroundColor: + Colors + .orange, + textColor: + Colors + .white, + ); + return; + } + + try { + final repository = + updatereviewRepository(); + await repository.updatereviews( + context: + context, + url: ConstsApi + .updatereview, + review: rating + .toString(), + serviceId: + service + .id + .toString(), + ); + + Navigator.of( + context, + ).pop(); + Fluttertoast.showToast( + msg: + 'Thank you for your rating!', + toastLength: + Toast + .LENGTH_SHORT, + gravity: + ToastGravity + .BOTTOM, + backgroundColor: + Colors + .green, + textColor: + Colors + .white, + ); + + indexController + ?.changeIndex( + 3, + ); + Get.offAllNamed( + RouterConts + .history, + arguments: { + 'historyTab': + 0, + }, + ); + } catch (e) { + Fluttertoast.showToast( + msg: + 'Failed to submit rating: $e', + toastLength: + Toast + .LENGTH_LONG, + gravity: + ToastGravity + .BOTTOM, + backgroundColor: + Colors + .red, + textColor: + Colors + .white, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: + Color( + 0xFF0066FF, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 20.83, + ), + ), + ), + child: const Text( + "Submit", + textAlign: + TextAlign + .center, + style: TextStyle( + fontFamily: + 'Gilroy-Bold', + fontWeight: + FontWeight + .w700, + fontSize: 20, + color: Colors + .white, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } + } else if (status == 404) { + if (context.mounted) { + String message = + 'Maximum bookings reached'; + if (jsonResponse['data'] != null) { + message = jsonResponse['data'] + .toString(); + } + + Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.orange, + textColor: Colors.white, + ); + + Get.offAllNamed( + RouterConts.packageList, + arguments: 1, + ); + } + } else { + if (context.mounted) { + String message = + 'Booking failed. Please try again.'; + if (jsonResponse['message'] != null) { + message = jsonResponse['message'] + .toString(); + } + + Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + } catch (e) { + print('Network/API Error: $e'); + print('Error Type: ${e.runtimeType}'); + + String errorMessage = + 'Connection failed. Please check:'; + + if (e.toString().contains( + 'SocketException', + ) || + e.toString().contains( + 'NetworkException', + )) { + errorMessage = + 'No internet connection. Please check your network.'; + } else if (e.toString().contains( + 'TimeoutException', + )) { + errorMessage = + 'Request timeout. Please try again.'; + } else if (e.toString().contains( + 'FormatException', + )) { + errorMessage = + 'Server returned invalid response. Please contact support.'; + } else { + errorMessage = + 'Booking failed: ${e.toString()}'; + } + + if (context.mounted) { + Fluttertoast.showToast( + msg: errorMessage, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + } + }, + child: const Text( + "Book Now", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 18, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Future _testApiConnectivity() async { + try { + final uri = Uri.parse(ConstsApi.bookservice); + final baseUrl = '${uri.scheme}://${uri.host}'; + + print('Testing connectivity to: $baseUrl'); + + final response = await http + .get( + Uri.parse(baseUrl), + headers: {'Content-Type': 'application/json'}, + ) + .timeout(const Duration(seconds: 10)); + + print('Connectivity test - Status: ${response.statusCode}'); + return response.statusCode < 500; + } catch (e) { + print('Connectivity test failed: $e'); + return false; + } + } + + void _scrollToField(int fieldIndex) { + final offset = 300.0 + (fieldIndex * 150.0); + + if (_scrollController.hasClients) { + _scrollController.animateTo( + offset, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + + Widget buildFieldTitle(String title) { + return Text( + title, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ); + } + + Widget buildTextFormField({ + required TextEditingController controller, + FocusNode? focusNode, // Add FocusNode parameter + required String hintText, + TextInputType? keyboardType, + int maxLines = 1, + Function()? onTap, + String? Function(String?)? validator, + }) { + return TextFormField( + controller: controller, + focusNode: focusNode, // Use FocusNode + keyboardType: keyboardType, + maxLines: maxLines, + onTap: onTap, + // SOLUTION 8: Remove onChanged that causes frequent rebuilds + // Only validate on form submission, not on every character change + decoration: InputDecoration( + hintText: hintText, + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0xFFB7B7B7), width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0xFFB7B7B7), width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0xFFB7B7B7), width: 1), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Colors.red, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Colors.red, width: 1), + ), + ), + style: const TextStyle(fontFamily: 'Gilroy-Medium', fontSize: 14), + validator: validator, + ); + } +} diff --git a/lib/view/user_main_screens/detail_service_page.dart b/lib/view/user_main_screens/detail_service_page.dart new file mode 100644 index 0000000..bd94fbb --- /dev/null +++ b/lib/view/user_main_screens/detail_service_page.dart @@ -0,0 +1,2243 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/user_main_screens/image_page.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:dotted_line/dotted_line.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import 'package:video_player/video_player.dart'; + +class DetailServicePage extends ConsumerStatefulWidget { + final int id; + const DetailServicePage({super.key, required this.id}); + + @override + ConsumerState createState() => _DetailServicePageState(); +} + +class _DetailServicePageState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + int selectedButton = 1; + final ScrollController _scrollController = ScrollController(); + final ScrollController localScrollController = ScrollController(); + int _currentServiceId = 0; + int selectedIndex = 1; + + // Store current detail for button access + dynamic currentDetail; + + // Video related variables + final List _controllers = []; + final List _youtubeControllers = []; + bool _isDisposed = false; + bool _controllersInitialized = false; + + // Form related variables + final _formKey = GlobalKey(); + AutovalidateMode _autoValidateMode = AutovalidateMode.disabled; + final nameController = TextEditingController(); + final phoneController = TextEditingController(); + final emailController = TextEditingController(); + final messageController = TextEditingController(); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _currentServiceId = widget.id; + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + _isDisposed = true; + WidgetsBinding.instance.removeObserver(this); + + // Dispose controllers safely + _disposeControllers(); + + // Dispose scroll controllers + _scrollController.dispose(); + localScrollController.dispose(); + + // Dispose text controllers + nameController.dispose(); + phoneController.dispose(); + emailController.dispose(); + messageController.dispose(); + + super.dispose(); + } + + // Required method for AutomaticKeepAliveClientMixin + @override + void didChangeMetrics() { + // Handle device metrics changes (screen rotation, keyboard, etc.) + if (mounted) { + debugPrint('Device metrics changed'); + } + } + + // Handle app lifecycle changes + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.paused: + _pauseAllVideos(); + break; + case AppLifecycleState.resumed: + break; + case AppLifecycleState.inactive: + break; + case AppLifecycleState.detached: + break; + case AppLifecycleState.hidden: + break; + } + } + + void _pauseAllVideos() { + try { + for (var controller in _controllers) { + if (controller.value.isInitialized && controller.value.isPlaying) { + controller.pause(); + } + } + } catch (e) { + debugPrint('Error pausing videos: $e'); + } + } + + void _disposeControllers() { + // Dispose video controllers + for (var controller in _controllers) { + try { + if (!controller.value.isInitialized) continue; + controller.pause(); + controller.dispose(); + } catch (e) { + debugPrint('Error disposing video controller: $e'); + } + } + _controllers.clear(); + + // Dispose YouTube controllers + for (var controller in _youtubeControllers) { + try { + controller.dispose(); + } catch (e) { + debugPrint('Error disposing YouTube controller: $e'); + } + } + _youtubeControllers.clear(); + + _controllersInitialized = false; + } + + // Method to check if URL is YouTube link + bool isYoutubeLink(String url) { + return url.contains('youtube.com') || + url.contains('youtu.be') || + url.contains('m.youtube.com'); + } + + // Method to check if URL is direct video + bool isDirectVideo(String url) { + return url.contains('.mp4') || + url.contains('.mov') || + url.contains('.avi') || + url.contains('.mkv') || + url.contains('.webm') || + url.contains('.3gp') || + url.contains('.flv'); + } + + // Initialize video controllers with better error handling + void _initializeVideoControllers(List videoUrls) { + if (_isDisposed || _controllersInitialized) return; + + // Dispose existing controllers first + _disposeControllers(); + + try { + for (String url in videoUrls) { + if (_isDisposed) break; + + if (isDirectVideo(url)) { + final controller = VideoPlayerController.network(url); + controller + .initialize() + .then((_) { + if (!_isDisposed && mounted) { + setState(() {}); + } + }) + .catchError((error) { + debugPrint('Error initializing video controller: $error'); + }); + _controllers.add(controller); + } else if (isYoutubeLink(url)) { + final videoId = YoutubePlayer.convertUrlToId(url); + if (videoId != null && videoId.isNotEmpty) { + try { + final youtubeController = YoutubePlayerController( + initialVideoId: videoId, + flags: const YoutubePlayerFlags( + autoPlay: false, + mute: false, + controlsVisibleAtStart: true, + ), + ); + _youtubeControllers.add(youtubeController); + } catch (e) { + debugPrint('Error creating YouTube controller: $e'); + } + } + } + } + _controllersInitialized = true; + } catch (e) { + debugPrint('Error in _initializeVideoControllers: $e'); + } + } + + // Method to update service and smooth scroll to top + void _updateService(dynamic item) { + if (_isDisposed) return; + + setState(() { + _currentServiceId = item.id ?? widget.id; + }); + + // Dispose old controllers before loading new service + _disposeControllers(); + + // Smooth scroll to top to show the new service details + if (_scrollController.hasClients) { + _scrollController.animateTo( + 0.0, + duration: const Duration(milliseconds: 800), + curve: Curves.easeInOut, + ); + } + } + + void _onFieldChanged() { + if (_autoValidateMode == AutovalidateMode.onUserInteraction && mounted) { + setState(() {}); // Triggers error revalidation and hiding + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 5.0, + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(width: 8), + const Text( + "Service Details", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + ], + ), + ), + Expanded( + child: Consumer( + builder: (context, ref, _) { + final detailAsyncValue = ref.watch( + detailpageProvider(_currentServiceId.toString()), + ); + + return detailAsyncValue.when( + data: (detailList) { + if (detailList.isEmpty) { + return const Center(child: Text("No details found.")); + } + + final detail = detailList.first; + + // Store current detail for button access + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isDisposed) { + currentDetail = detail; + } + }); + + final imageUrls = detail.images1 ?? []; + final videoUrls = detail.videos ?? []; + final categoryId = detail.category.toString() ?? '0'; + + // Initialize video controllers only once per service + if (!_controllersInitialized && videoUrls.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isDisposed) { + _initializeVideoControllers(videoUrls); + } + }); + } + + final mostPopularAsyncValue = ref.watch( + mostPopularProvider(categoryId), + ); + + return SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Animated container for smooth transition + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: Stack( + key: ValueKey(_currentServiceId), + clipBehavior: Clip.none, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: imageUrls.isNotEmpty + ? CachedNetworkImage( + imageUrl: imageUrls[0], + width: double.infinity, + height: 250, + fit: BoxFit.cover, + placeholder: (context, url) => + Container( + width: double.infinity, + height: 250, + color: Colors.grey[300], + child: const Center( + child: Icon( + Icons.image, + size: 50, + color: Colors.grey, + ), + ), + ), + errorWidget: + ( + context, + url, + error, + ) => Container( + width: double.infinity, + height: 250, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + size: 50, + color: Colors.grey, + ), + ), + ) + : Container( + width: double.infinity, + height: 250, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + size: 50, + color: Colors.grey, + ), + ), + ), + ), + Positioned( + bottom: -100, + left: 25, + right: 25, + child: AnimatedContainer( + duration: const Duration( + milliseconds: 300, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFCFCFC), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: const Color(0xFFC9C9C9), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.1, + ), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + detail.vendorName ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + letterSpacing: 0.2, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 5), + Text( + detail.servicename ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w600, + fontSize: 16, + letterSpacing: 0.18, + color: Color(0xFF5A5A5A), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 15), + const DottedLine( + dashColor: Color(0xFFBABABA), + lineThickness: 1, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const Text( + "Payment:", + style: TextStyle( + fontFamily: + 'Gilroy-Bold', + fontWeight: + FontWeight.w700, + fontSize: 15.61, + ), + ), + const SizedBox( + height: 15, + ), + Text( + 'Rs ${detail.amount ?? 0}', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w500, + fontSize: 14, + color: Color( + 0xFF636363, + ), + ), + ), + ], + ), + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const Text( + "Duration:", + style: TextStyle( + fontFamily: + 'Gilroy-Bold', + fontWeight: + FontWeight.w700, + fontSize: 15.61, + ), + ), + const SizedBox( + height: 15, + ), + Text( + detail.workingduration ?? + '', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w500, + fontSize: 14, + color: Color( + 0xFF636363, + ), + ), + ), + ], + ), + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const Text( + "Rating:", + style: TextStyle( + fontFamily: + 'Gilroy-Bold', + fontWeight: + FontWeight.w700, + fontSize: 15.61, + ), + ), + const SizedBox( + height: 15, + ), + Row( + children: [ + const Icon( + Icons.star, + size: 16, + color: Colors.amber, + ), + const SizedBox( + width: 4, + ), + Text( + (((double.tryParse( + detail.averageReview ?? + '0', + ) ?? + 0.0) * + 2) + .floor() / + 2) + .toStringAsFixed( + 1, + ), + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight + .w500, + fontSize: 14, + color: Color( + 0xFF636363, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 120), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Container( + width: double.infinity, + height: 93, + decoration: BoxDecoration( + color: const Color(0xFFFAFCFF), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: const Color(0xFFF1F1F1), + width: 1, + ), + ), + child: Row( + children: [ + const SizedBox(width: 12), + ClipOval( + child: CachedNetworkImage( + imageUrl: + detail.profilePic?.toString() ?? '', + placeholder: (context, url) => + Container( + width: 67, + height: 67, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey, + ), + child: const Icon( + Icons.person, + color: Colors.white, + ), + ), + errorWidget: (context, url, error) => + Container( + width: 67, + height: 67, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey, + ), + child: const Icon( + Icons.person, + color: Colors.white, + ), + ), + width: 55, + height: 55, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + detail.vendorname ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 20, + color: Color(0xFF353434), + ), + ), + const SizedBox(height: 4), + Text( + detail.categoryName ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: 15, + color: Color(0xFF717171), + ), + ), + ], + ), + ), + Row( + children: [ + const Icon( + Icons.star, + color: Colors.orange, + size: 18, + ), + const SizedBox(width: 4), + Text( + (((double.tryParse( + detail.averageReview ?? + '0', + ) ?? + 0.0) * + 2) + .floor() / + 2) + .toStringAsFixed(1), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF636363), + ), + ), + + const SizedBox(width: 12), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 10), + const SizedBox(height: 10), + // Two Buttons (Description / Review Switch) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Container( + width: double.infinity, + height: 75, + decoration: BoxDecoration( + color: const Color(0xFFFAFCFF), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: const Color(0xFFF1F1F1), + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Description Button + GestureDetector( + onTap: () { + if (mounted) { + setState(() { + selectedButton = 1; + }); + } + }, + child: Container( + width: 130, + height: 48, + decoration: BoxDecoration( + color: selectedButton == 1 + ? AppColors.primary + : Colors.white, + borderRadius: BorderRadius.circular( + 10, + ), + border: Border.all( + color: selectedButton == 1 + ? AppColors.primary + : const Color(0xFFCFCFCF), + width: 1, + ), + ), + child: Center( + child: Text( + 'Description', + style: TextStyle( + color: selectedButton == 1 + ? Colors.white + : Colors.black, + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 15, + height: 13.17 / 18, + letterSpacing: 0.01 * 18, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + // Review Button + GestureDetector( + onTap: () { + if (mounted) { + setState(() { + selectedButton = 2; + }); + ref.refresh( + getreviewuserProvider( + detail.id.toString(), + ), + ); + } + }, + child: Container( + width: 130, + height: 48, + decoration: BoxDecoration( + color: selectedButton == 2 + ? AppColors.primary + : Colors.white, + borderRadius: BorderRadius.circular( + 10, + ), + border: Border.all( + color: selectedButton == 2 + ? AppColors.primary + : const Color(0xFFCFCFCF), + width: 1, + ), + ), + child: Center( + child: Text( + 'Review', + style: TextStyle( + color: selectedButton == 2 + ? Colors.white + : Colors.black, + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 15, + height: 13.17 / 18, + letterSpacing: 0.01 * 18, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 20), + + // Switch between Description and Review + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + ), + child: selectedButton == 1 + ? Row( + children: [ + Expanded( + child: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'Description: ', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w600, + fontSize: 16, + height: 28 / 16, + letterSpacing: 0, + ), + ), + TextSpan( + text: + detail.description ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 16, + height: 28 / 16, + letterSpacing: 0, + ), + ), + ], + ), + ), + ), + ], + ) + : Consumer( + builder: (context, ref, _) { + final reviewsAsync = ref.watch( + getreviewuserProvider( + detail.id.toString(), + ), + ); + return reviewsAsync.when( + data: (reviews) { + if (reviews.isEmpty) { + return Center( + child: Container( + padding: const EdgeInsets.all( + 16, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular( + 22, + ), + border: Border.all( + color: + Colors.grey.shade300, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.25), + offset: const Offset( + 0, + 1, + ), + blurRadius: 4.3, + ), + ], + ), + child: const Text( + 'No reviews available for this service yet.', + style: TextStyle( + fontFamily: + 'Gilroy-Medium', + fontSize: 16, + ), + ), + ), + ); + } + + return Container( + width: double.infinity, + height: 150, + margin: const EdgeInsets.only( + bottom: 16, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(22), + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.25), + offset: const Offset(0, 1), + blurRadius: 4.3, + ), + ], + ), + child: Scrollbar( + controller: + localScrollController, + thumbVisibility: true, + thickness: 6.0, + radius: const Radius.circular( + 10, + ), + child: SingleChildScrollView( + controller: + localScrollController, + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: reviews.map(( + review, + ) { + return Padding( + padding: + const EdgeInsets.only( + bottom: 8.0, + ), + child: Row( + children: [ + ClipOval( + child: CachedNetworkImage( + imageUrl: + review + .profilePic1 + ?.toString() ?? + '', + placeholder: (context, url) => Container( + width: 39, + height: 39, + decoration: const BoxDecoration( + shape: BoxShape + .circle, + color: Colors + .grey, + ), + child: const Icon( + Icons + .person, + size: 20, + color: Colors + .white, + ), + ), + errorWidget: + ( + context, + url, + error, + ) => Container( + width: 39, + height: + 39, + decoration: const BoxDecoration( + shape: BoxShape + .circle, + color: Colors + .grey, + ), + child: const Icon( + Icons + .person, + size: + 20, + color: Colors + .white, + ), + ), + width: 39, + height: 39, + fit: BoxFit + .cover, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + review.userName ?? + '', + style: const TextStyle( + fontFamily: + 'Gilroy-Bold', + fontWeight: + FontWeight + .w700, + fontSize: 20, + height: + 13.17 / + 20, + letterSpacing: + 0.2, + ), + ), + ), + Row( + children: List.generate( + int.tryParse( + review.review ?? + '0', + ) ?? + 0, + ( + index, + ) => const Icon( + Icons.star, + color: Colors + .amber, + size: 20, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ), + ); + }, + loading: () => SizedBox( + height: 150, + child: const Center( + child: Text('Loading reviews...'), + ), + ), + error: (error, stack) => SizedBox( + height: 150, + child: Center( + child: Text( + 'Unable to load reviews', + style: const TextStyle( + color: Colors.red, + ), + ), + ), + ), + ); + }, + ), + ), + + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FloatingActionButton( + onPressed: () { + // TODO: Add phone call logic + }, + backgroundColor: Colors.green, // Optional + shape: + const CircleBorder(), // Ensures it's a perfect circle + child: const Icon( + Icons.call, + color: Colors.white, + ), + ), + Image.asset(AppAssets.map), + Image.asset(AppAssets.share), + ], + ), + ), + // Images Section + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Image :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.0, + letterSpacing: 1.0, + ), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ImagePage(mediaUrls: imageUrls), + ), + ); + }, + child: const Text( + "View all", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 17, + height: 1.0, + letterSpacing: 1.0, + ), + ), + ), + ], + ), + ), + + // Images ListView + SizedBox( + height: 120, + child: imageUrls.isEmpty + ? const Center( + child: Text("No images available"), + ) + : ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: imageUrls.length, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + itemBuilder: (context, index) { + return Container( + width: 120, + margin: const EdgeInsets.only( + right: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 15, + ), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 15, + ), + child: CachedNetworkImage( + imageUrl: imageUrls[index], + fit: BoxFit.cover, + placeholder: (context, url) => + Container( + color: Colors.grey[200], + child: const Center( + child: Icon( + Icons.image, + color: Colors.grey, + size: 30, + ), + ), + ), + errorWidget: + (context, url, error) => + Container( + color: Colors.grey[200], + child: const Center( + child: Icon( + Icons.error, + color: Colors.red, + size: 30, + ), + ), + ), + ), + ), + ); + }, + ), + ), + + // Videos Section + if (videoUrls.isNotEmpty) ...[ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + "Videos :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.0, + letterSpacing: 1.0, + ), + ), + ), + + SizedBox( + height: 150, + child: ListView.builder( + key: ValueKey( + 'video-list-$_currentServiceId', + ), + scrollDirection: Axis.horizontal, + itemCount: videoUrls.length, + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + itemBuilder: (context, index) { + final url = videoUrls[index]; + + if (isYoutubeLink(url)) { + final videoId = + YoutubePlayer.convertUrlToId(url) ?? + ''; + if (videoId.isEmpty) { + return Container( + width: 113.14, + height: 101.83, + margin: const EdgeInsets.only( + right: 10, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 14.71, + ), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: const Text( + "Invalid YouTube URL", + ), + ); + } + + // Use pre-created controller if available + YoutubePlayerController? + youtubeController; + if (index < _youtubeControllers.length) { + youtubeController = + _youtubeControllers[index]; + } else { + youtubeController = + YoutubePlayerController( + initialVideoId: videoId, + flags: const YoutubePlayerFlags( + autoPlay: false, + mute: false, + ), + ); + } + + return Container( + width: 113.14, + height: 101.83, + margin: const EdgeInsets.only( + right: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 14.71, + ), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 14.71, + ), + child: YoutubePlayer( + controller: youtubeController, + showVideoProgressIndicator: true, + ), + ), + ); + } else if (isDirectVideo(url)) { + // Find the matching controller for this URL + VideoPlayerController? controller; + if (index < _controllers.length) { + controller = _controllers[index]; + } + + return GestureDetector( + onTap: () { + if (controller != null && + controller.value.isInitialized) { + if (mounted) { + setState(() { + if (controller! + .value + .isPlaying) { + controller.pause(); + } else { + for (var c in _controllers) { + if (c != controller && + c.value.isPlaying) { + c.pause(); + } + } + controller.play(); + } + }); + } + } + }, + child: Container( + width: 113.14, + height: 101.83, + margin: const EdgeInsets.only( + right: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 14.71, + ), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 14.71, + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (controller != null && + controller + .value + .isInitialized) + AspectRatio( + aspectRatio: controller + .value + .aspectRatio, + child: VideoPlayer( + controller, + ), + ) + else + Container( + color: Colors.grey[300], + child: const Center( + child: Text('Loading...'), + ), + ), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.black + .withOpacity(0.5), + shape: BoxShape.circle, + ), + child: Icon( + controller != null && + controller + .value + .isPlaying + ? Icons.pause + : Icons.play_arrow, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ); + } else { + return Container( + width: 113.14, + height: 101.83, + margin: const EdgeInsets.only( + right: 10, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 14.71, + ), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: const Text("Invalid video"), + ); + } + }, + ), + ), + ], + + // Relevant Services Section + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + "Relevant Service :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.0, + letterSpacing: 1.0, + ), + ), + ), + const SizedBox(height: 20), + + // Dynamic most popular services from API + SizedBox( + height: 210, + child: mostPopularAsyncValue.when( + data: (services) { + // Filter out the current service from relevant services + final filteredServices = services + .where( + (service) => + service.id != _currentServiceId, + ) + .toList(); + + return filteredServices.isEmpty + ? const Center( + child: Text( + 'No relevant services found', + ), + ) + : ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: filteredServices.length, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + itemBuilder: (context, index) { + final item = + filteredServices[index]; + bool isSelected = + item.id == _currentServiceId; + + return AnimatedContainer( + duration: const Duration( + milliseconds: 300, + ), + width: 180, + margin: const EdgeInsets.only( + right: 16, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? AppColors.primary + : AppColors.lightGrey, + width: isSelected ? 2 : 1, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: AppColors + .primary + .withOpacity(0.3), + blurRadius: 8, + offset: const Offset( + 0, + 4, + ), + ), + ] + : [], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: + const BorderRadius.vertical( + top: Radius.circular( + 12, + ), + ), + child: + (item.images1 != null && + item + .images1! + .isNotEmpty) + ? CachedNetworkImage( + imageUrl: item + .images1! + .first, + width: + double.infinity, + height: 100, + fit: BoxFit.cover, + placeholder: + ( + context, + url, + ) => Container( + width: double + .infinity, + height: 100, + color: Colors + .grey[300], + child: const Icon( + Icons.image, + color: Colors + .grey, + ), + ), + errorWidget: + ( + context, + url, + error, + ) => Container( + width: double + .infinity, + height: 100, + color: Colors + .grey[300], + child: const Icon( + Icons + .image_not_supported, + color: Colors + .grey, + ), + ), + ) + : Container( + width: + double.infinity, + height: 100, + color: Colors + .grey[300], + child: const Icon( + Icons + .image_not_supported, + color: + Colors.grey, + ), + ), + ), + Align( + alignment: + Alignment.centerRight, + child: Container( + height: 30, + width: 80, + transform: + Matrix4.translationValues( + -10, + -15, + 0, + ), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: + BorderRadius.circular( + 20, + ), + border: Border.all( + color: Colors.white, + width: 1.5, + ), + ), + child: Center( + child: Text( + 'Rs.${item.amount ?? "N/A"}', + style: const TextStyle( + fontFamily: + 'Gilroy-Bold', + fontWeight: + FontWeight.w400, + fontSize: 16.86, + height: + 15.17 / 16.86, + letterSpacing: 0.02, + color: Color( + 0xFFFCFAFA, + ), + ), + ), + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: Text( + item.serviceName ?? + 'Service', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight + .w700, + fontSize: + 15.02, + height: + 16.22 / + 18.02, + letterSpacing: + 0.0, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + Row( + children: [ + const Icon( + Icons.star, + color: Colors + .orange, + size: 18, + ), + const SizedBox( + width: 4, + ), + Text( + (((double.tryParse( + item.averageReview?.toString() ?? + '0', + ) ?? + 0.0) * + 2) + .floor() / + 2) + .toStringAsFixed( + 1, + ), + textAlign: + TextAlign + .center, + style: const TextStyle( + fontFamily: + 'SF UI Display', + fontWeight: + FontWeight + .w600, + fontSize: + 14.17, + height: + 20.09 / + 14.17, + letterSpacing: + -0.5, + ), + ), + ], + ), + ], + ), + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Container( + width: 125, + padding: + const EdgeInsets.all( + 4, + ), + decoration: BoxDecoration( + border: Border.all( + color: AppColors + .lightGrey, + ), + color: Colors + .white, + borderRadius: + BorderRadius.circular( + 12, + ), + ), + child: Row( + children: [ + item.profilePic1 != + null + ? CircleAvatar( + radius: + 15, + backgroundImage: NetworkImage( + item.profilePic1!, + ), + ) + : const CircleAvatar( + radius: + 15, + backgroundImage: AssetImage( + AppAssets.login, + ), + ), + const SizedBox( + width: 10, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + item.vendorName ?? + 'Vendor', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w400, + fontSize: + 11.75, + height: + 10.57 / + 11.75, + letterSpacing: + 0.0, + color: Color( + 0xFF353434, + ), + ), + maxLines: + 1, + overflow: + TextOverflow.ellipsis, + ), + const SizedBox( + height: + 5, + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: + 75, + ), + child: Text( + item.serviceName ?? + 'Service', + style: const TextStyle( + fontFamily: + 'Gilroy-Regular', + fontWeight: + FontWeight.w400, + fontSize: + 11.75, + height: + 10.57 / + 11.75, + letterSpacing: + 0.0, + color: Color( + 0xFF717171, + ), + ), + maxLines: + 1, + overflow: + TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + GestureDetector( + onTap: () => + _updateService( + item, + ), + child: AnimatedContainer( + duration: + const Duration( + milliseconds: + 200, + ), + padding: + const EdgeInsets.all( + 8, + ), + decoration: BoxDecoration( + color: AppColors + .primary, + borderRadius: + BorderRadius.circular( + 12, + ), + ), + child: Icon( + isSelected + ? Icons + .check_rounded + : Icons + .arrow_forward_ios_rounded, + size: 16, + color: Colors + .white, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + loading: () => const SizedBox( + height: 210, + child: Center( + child: Text('Loading services...'), + ), + ), + error: (error, stack) => SizedBox( + height: 210, + child: Center( + child: Text( + 'Error loading services', + style: TextStyle(color: Colors.red), + ), + ), + ), + ), + ), + + // Two Buttons Section + Center( + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16, + top: 40, + ), + child: Row( + children: [ + Expanded(child: buildButton("Enquiry", 0)), + const SizedBox(width: 16), + Expanded(child: buildButton("Book now", 1)), + ], + ), + ), + ), + const SizedBox(height: 50), // Bottom padding + ], + ), + ); + }, + loading: () => const Center( + child: Padding( + padding: EdgeInsets.all(50.0), + child: Text('Loading service details...'), + ), + ), + error: (err, stack) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + "Failed to load service details", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + "Please check your connection and try again", + style: TextStyle(color: Colors.grey[600]), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.refresh( + detailpageProvider( + _currentServiceId.toString(), + ), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + + Widget buildButton(String text, int index) { + final bool isSelected = selectedIndex == index; + + return Container( + height: 66, + decoration: BoxDecoration( + color: isSelected ? Color(0xFF0066FF) : Colors.white, + borderRadius: BorderRadius.circular(48), + border: Border.all(color: Color(0xFFCFCFCF)), + ), + child: TextButton( + onPressed: () { + if (mounted) { + setState(() { + selectedIndex = index; + }); + + if (index == 0) { + // Show enquiry bottom sheet + _showEnquiryBottomSheet(); + } else { + // Navigate to booking page with current detail + if (currentDetail != null) { + Get.toNamed( + RouterConts.bookingserivce, + arguments: currentDetail, + ); + } else { + // Fallback: Get detail from provider + final detailAsyncValue = ref.read( + detailpageProvider(_currentServiceId.toString()), + ); + detailAsyncValue.whenData((detailList) { + if (detailList.isNotEmpty && mounted) { + Get.toNamed( + RouterConts.bookingserivce, + arguments: detailList.first, + ); + } + }); + } + } + } + }, + child: Text( + text, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 18, + height: 21.5 / 23.89, + letterSpacing: 0.0, + color: isSelected ? Colors.white : const Color(0xFF292929), + ), + ), + ), + ); + } + + void _showEnquiryBottomSheet() { + if (!mounted) return; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(40)), + ), + backgroundColor: Colors.white, + builder: (BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 24.0, + bottom: MediaQuery.of(context).viewInsets.bottom + 24.0, + ), + child: SizedBox( + width: double.infinity, + height: 600, // Increased height for 5 fields + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Custom app bar-like header + Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back_ios_new, size: 20), + ), + const Expanded( + child: Center( + child: Text( + "Enquiry", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + // Invisible icon to balance the row for perfect center alignment + const Opacity( + opacity: 0, + child: Icon(Icons.arrow_back_ios_new, size: 20), + ), + ], + ), + const SizedBox(height: 24), + Expanded( + child: SingleChildScrollView( + child: Form( + key: _formKey, + autovalidateMode: _autoValidateMode, + child: Column( + children: [ + _buildField( + label: "Name", + controller: nameController, + validator: (value) => + value == null || value.trim().isEmpty + ? "Name is required" + : null, + ), + const SizedBox(height: 16), + _buildField( + label: "Mobile Number", + controller: phoneController, + keyboardType: TextInputType.phone, + validator: (value) => + value == null || value.length != 10 + ? "Enter valid mobile number" + : null, + ), + const SizedBox(height: 16), + _buildField( + label: "Email", + controller: emailController, + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return "Email is required"; + } + final regex = RegExp(r'^[^@]+@[^@]+\.[^@]+'); + return regex.hasMatch(value) + ? null + : "Enter a valid email"; + }, + ), + + const SizedBox(height: 16), + _buildField( + label: "Message", + controller: messageController, + maxLines: 4, + height: 120, + validator: (value) => value == null || value.isEmpty + ? "Message is required" + : null, + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 55.38, + child: ElevatedButton( + onPressed: () async { + if (mounted) { + setState(() { + _autoValidateMode = + AutovalidateMode.onUserInteraction; + }); + + if (_formKey.currentState!.validate()) { + try { + final repo = ref.read( + enquriyupdateRepositoryProvider, + ); + + await repo.updateEnquriy( + context: context, + url: ConstsApi.enquery, + name: nameController.text.trim(), + number: phoneController.text.trim(), + email: emailController.text.trim(), + + message: messageController.text.trim(), + serviceid: _currentServiceId.toString(), + ); + + // Clear form fields + nameController.clear(); + phoneController.clear(); + emailController.clear(); + + messageController.clear(); + + if (mounted) { + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: 'Submission failed: $e', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.52), + ), + ), + child: const Text( + "Submit", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20.17, + height: 1.25, + letterSpacing: 1, + color: AppColors.secondprimary, + ), + ), + ), + ), + const SizedBox(height: 16), // Extra bottom padding + ], + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildField({ + required String label, + required TextEditingController controller, + required String? Function(String?) validator, + TextInputType keyboardType = TextInputType.text, + int maxLines = 1, + double height = 58, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 380, + height: height, + decoration: BoxDecoration( + color: const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFE1E1E1)), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.center, + child: TextFormField( + controller: controller, + keyboardType: keyboardType, + maxLines: maxLines, + decoration: InputDecoration( + hintText: label, + border: InputBorder.none, + ), + validator: validator, + onChanged: (_) => _onFieldChanged(), + ), + ), + const SizedBox(height: 4), + ], + ); + } +} diff --git a/lib/view/user_main_screens/history_screen/Service_Booking.dart b/lib/view/user_main_screens/history_screen/Service_Booking.dart new file mode 100644 index 0000000..7222a04 --- /dev/null +++ b/lib/view/user_main_screens/history_screen/Service_Booking.dart @@ -0,0 +1,932 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/model/booking_modify_model.dart'; +import 'package:bookmywages/model/cancel_booking.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +class ServiceBooking extends ConsumerStatefulWidget { + const ServiceBooking({super.key}); + + @override + ConsumerState createState() => _ServiceBookingState(); +} + +class _ServiceBookingState extends ConsumerState { + @override + Widget build(BuildContext context) { + final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: bookingAsyncValue.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text('Error: $err')), + data: (bookings) { + return ListView.builder( + itemCount: bookings.length, + shrinkWrap: false, + itemBuilder: (context, index) { + final booking = bookings[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Top Row: ID and "View order" + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + // Update the onTap method in the ServiceBooking class + GestureDetector( + onTap: () { + Get.toNamed( + RouterConts.detailserivce, + arguments: booking.serviceId, + ); + }, + child: Text( + 'View order', + style: TextStyle( + color: Colors.blue.shade600, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + + const Divider(), + + /// Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: (booking.images1.isNotEmpty) + ? Image.network( + booking.images1[0], + width: 100, + height: 110, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + value: + loadingProgress + .expectedTotalBytes != + null + ? loadingProgress + .cumulativeBytesLoaded / + (loadingProgress + .expectedTotalBytes ?? + 1) + : null, + ), + ); + }, + errorBuilder: + (context, error, stackTrace) { + return const Icon( + Icons.error, + size: 100, + ); + }, + ) + : const Icon( + Icons.image_not_supported, + size: 100, + ), + ), + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.vendorName ?? + 'Arun Cleaning Company', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + booking.serviceName ?? + 'Cleaning service', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 1.3, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + ), + + const SizedBox(width: 10), + // Updated status display code in the ServiceBooking class + Expanded( + child: Container( + width: booking.status == 3 + ? 70 + : booking.status == 1 + ? 85 // Width for Scheduled + : 77.84, // Width for Pending and other statuses + height: booking.status == 3 + ? 25 + : booking.status == 1 + ? 25 // Height for Scheduled + : 27.96, // Height for Pending and other statuses + decoration: BoxDecoration( + color: booking.status == 3 + ? const Color( + 0xFFFFEEEE, + ) // #FFEEEE for Cancel + : booking.status == 1 + ? const Color( + 0xFFE6F7E6, + ) // Light green for Scheduled + : const Color( + 0xFFDAE9FF, + ), // #DAE9FF for Pending + borderRadius: BorderRadius.circular( + 6.05, + ), // 6.05px border radius + ), + child: Center( + child: Text( + booking.status == 3 + ? 'Cancel' + : booking.status == 1 + ? 'Scheduled' // Text for status 1 + : 'Pending', // Text for other statuses + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: booking.status == 3 + ? const Color( + 0xFFFF0000, + ) // Red color for Cancel + : booking.status == 1 + ? const Color( + 0xFF2E8B57, + ) // Green color for Scheduled + : const Color( + 0xFF0066FF, + ), // Blue color for Pending + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.lightGrey, + ), + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 15, + backgroundImage: + booking.profilePic != null && + booking.profilePic!.isNotEmpty + ? NetworkImage( + booking.profilePic.toString(), + ) + : null, + child: + booking.profilePic == null || + booking.profilePic!.isEmpty + ? const Icon( + Icons.person, + size: 20, + ) + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + booking.vendorName ?? + 'no data', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w600, + fontSize: 11, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color( + 0xFF353434, + ), + ), + overflow: + TextOverflow.visible, + ), + ), + const Icon( + Icons.star, + color: Colors.orange, + size: 15, + ), + + const SizedBox(width: 4), + // Fixed: Changed to properly handle isRated as an integer + Text( + '${booking.isRated}', + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: + 'SF UI Display', + fontWeight: + FontWeight.w800, + fontSize: 10, + height: 12.5 / 8.82, + letterSpacing: -0.31, + ), + ), + ], + ), + SizedBox(height: 7), + Text( + booking.categoryName, + style: const TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: 11.84, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color(0xFF717171), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + + const SizedBox(height: 10), + Text( + booking.serviceDate ?? 'April 23, 2024', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceTime ?? '12:00 PM', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Working hours :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(height: 10), + Text( + booking.workingHours, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + /// Canceled Booking Button + /// Buttons Section - Modified for Scheduled Status + booking.status != 3 + ? Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + child: Row( + mainAxisAlignment: booking.status == 1 + ? MainAxisAlignment + .end // Align to the right for Scheduled status + : MainAxisAlignment + .spaceBetween, // Normal alignment for other statuses + children: [ + // Only show Modification button if status is NOT Scheduled (status 1) + booking.status != 1 + ? Expanded( + child: SizedBox( + height: 42, + child: ElevatedButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 20, + ), + ), + child: Container( + width: double.maxFinite, + decoration: BoxDecoration( + color: AppColors + .secondprimary, + borderRadius: + BorderRadius.circular( + 16, + ), + ), + child: + BookingModificationDialog( + id: booking.id + .toString(), + serviceId: booking + .serviceId + .toString(), + ), + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF0066FF, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(20), + ), + ), + child: const FittedBox( + child: Text( + "Modification", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w800, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.289, + color: Colors.white, + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(), // Empty space when Scheduled + + booking.status != 1 + ? const SizedBox(width: 8) + : const SizedBox.shrink(), + + // Cancel Button - Always visible + GestureDetector( + onTap: () async { + final data = CancelBookingRequest( + id: booking.id.toString(), + serviceId: booking.serviceId.toString(), + type: booking.type == 0 + ? "0" + : booking.type.toString(), + ); + + try { + // Call the cancel API + await ref.read( + cancelbookingProvider(data).future, + ); + + // Show success message + Fluttertoast.showToast( + msg: 'Booking cancelled successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + + // Refresh the booking history data + ref.invalidate( + userbookinghistorydetailsProvider, + ); + } catch (e) { + // Show error message + Fluttertoast.showToast( + msg: 'Failed to cancel booking: $e', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + }, + + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Cancel Booking', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + }, + ), + ), + ); + } + + // Updated _loadBookingImage and related methods to handle images properly + + // Check if URL is likely to cause a 500 error +} + +class BookingModificationDialog extends ConsumerStatefulWidget { + final String id; + final String serviceId; + + const BookingModificationDialog({ + super.key, + required this.id, + required this.serviceId, + }); + + @override + _BookingModificationDialogState createState() => + _BookingModificationDialogState(); +} + +class _BookingModificationDialogState + extends ConsumerState { + int selectedDateIndex = 0; + int selectedTimeIndex = 0; + + DateTime currentMonth = DateTime.now(); + late List dateList; + List times = []; + + @override + void initState() { + super.initState(); + _generateDateList(); + _generateTimes(); + } + + void _generateDateList() { + final firstDay = DateTime(currentMonth.year, currentMonth.month, 1); + final lastDay = DateTime(currentMonth.year, currentMonth.month + 1, 0); + + dateList = List.generate( + lastDay.day, + (index) => firstDay.add(Duration(days: index)), + ); + selectedDateIndex = 0; + } + + void _generateTimes() { + times = []; + for (int hour = 0; hour < 24; hour++) { + for (int minute = 0; minute < 60; minute += 30) { + final time = DateTime(0, 0, 0, hour, minute); + times.add(DateFormat('h:mm a').format(time)); + } + } + } + + void _goToPreviousMonth() { + final newMonth = DateTime(currentMonth.year, currentMonth.month - 1); + if (newMonth.isBefore( + DateTime(DateTime.now().year, DateTime.now().month, 1), + )) { + return; + } + + setState(() { + currentMonth = newMonth; + _generateDateList(); + }); + } + + void _goToNextMonth() { + setState(() { + currentMonth = DateTime(currentMonth.year, currentMonth.month + 1); + _generateDateList(); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 20, + left: 20, + right: 20, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Booking Modification', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + SizedBox(height: 20), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Change Date', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + Icon(Icons.calendar_today), + ], + ), + SizedBox(height: 10), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: _goToPreviousMonth, + icon: Icon(Icons.arrow_back_ios), + ), + Text( + DateFormat('MMMM yyyy').format(currentMonth), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + IconButton( + onPressed: _goToNextMonth, + icon: Icon(Icons.arrow_forward_ios), + ), + ], + ), + SizedBox(height: 10), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(dateList.length, (index) { + final date = dateList[index]; + final dayLabel = + '${DateFormat('E').format(date)}\n${DateFormat('d').format(date)}'; + final isSelected = selectedDateIndex == index; + + return GestureDetector( + onTap: () { + setState(() { + selectedDateIndex = index; + }); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 5), + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + color: isSelected ? Colors.blue : Colors.grey.shade200, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + dayLabel, + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }), + ), + ), + SizedBox(height: 20), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Change Time', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + Icon(Icons.access_time), + ], + ), + SizedBox(height: 10), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(times.length, (index) { + final isSelected = selectedTimeIndex == index; + return GestureDetector( + onTap: () { + setState(() { + selectedTimeIndex = index; + }); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 5), + padding: EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: isSelected ? Colors.blue : Colors.grey.shade200, + borderRadius: BorderRadius.circular(30), + ), + child: Text( + times[index], + style: TextStyle( + color: isSelected ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }), + ), + ), + SizedBox(height: 20), + + // GestureDetector( + // onTap: () { + // Navigator.pop(context); + // }, + // child: Container( + // width: double.infinity, + // padding: EdgeInsets.all(15), + // decoration: BoxDecoration( + // color: Colors.red.shade50, + // borderRadius: BorderRadius.circular(15), + // ), + // child: Center( + // child: Text( + // 'Cancel the Booking', + // style: TextStyle( + // color: Colors.red, + // fontWeight: FontWeight.bold, + // fontSize: 16, + // ), + // ), + // ), + // ), + // ), + SizedBox(height: 20), + + ElevatedButton( + onPressed: () async { + final selectedDate = DateFormat( + 'yyyy-MM-dd', + ).format(dateList[selectedDateIndex]); + final selectedTime = times[selectedTimeIndex]; + + final model = BookingModifyModel( + id: widget.id, + serviceId: widget.serviceId, + servicedate: selectedDate, + servicetime: selectedTime, + ); + + final result = await ref + .read(modifybookingProvider(model).future) + .catchError((e) { + Fluttertoast.showToast( + msg: 'Error: ${e.toString()}', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + return false; + }); + + if (result) { + Fluttertoast.showToast( + msg: 'Booking modified successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + Navigator.pop(context); + ref.invalidate(userbookinghistorydetailsProvider); + } else { + Fluttertoast.showToast( + msg: 'Failed to modify booking', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + }, + + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + padding: EdgeInsets.symmetric(horizontal: 100, vertical: 15), + ), + child: Text('Save', style: TextStyle(fontSize: 18)), + ), + SizedBox(height: 20), + ], + ), + ), + ); + } +} diff --git a/lib/view/user_main_screens/history_screen/canceled_page.dart b/lib/view/user_main_screens/history_screen/canceled_page.dart new file mode 100644 index 0000000..3053e07 --- /dev/null +++ b/lib/view/user_main_screens/history_screen/canceled_page.dart @@ -0,0 +1,460 @@ + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:intl/intl.dart'; +// Make sure to import your custom colors and dialog widget +// Assuming this exists + +class CanceledPage extends ConsumerWidget { + // Changed to ConsumerWidget + const CanceledPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Added WidgetRef parameter + final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: bookingAsyncValue.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text('Error: $err')), + data: (bookings) { + // Filter only canceled bookings (status == 3) + final canceledBookings = + bookings.where((booking) => booking.status == 3).toList(); + + if (canceledBookings.isEmpty) { + return const Center( + child: Text( + 'No canceled bookings found', + style: TextStyle(fontSize: 16), + ), + ); + } + + return ListView.builder( + itemCount: canceledBookings.length, + shrinkWrap: false, + itemBuilder: (context, index) { + final booking = canceledBookings[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Top Row: ID and "View order" + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + + const Divider(), + + /// Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (booking.images1.isNotEmpty) + ? Image.network( + booking.images1[0], + width: 100, + height: 110, + fit: BoxFit.cover, + loadingBuilder: ( + context, + child, + loadingProgress, + ) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + value: + loadingProgress + .expectedTotalBytes != + null + ? loadingProgress + .cumulativeBytesLoaded / + (loadingProgress + .expectedTotalBytes ?? + 1) + : null, + ), + ); + }, + errorBuilder: ( + context, + error, + stackTrace, + ) { + return const Icon( + Icons.error, + size: 100, + ); + }, + ) + : const Icon( + Icons.image_not_supported, + size: 100, + ), + ), + + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.vendorName ?? 'No vendor name', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + booking.serviceName ?? + 'No service name', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 12.52 / 13.91, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + + const SizedBox(width: 10), + Container( + width: 70, + height: 25, + decoration: BoxDecoration( + color: const Color(0xFFFFEEEE), + borderRadius: BorderRadius.circular( + 6.05, + ), + ), + child: Center( + child: Text( + 'Cancel', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: const Color(0xFFFF0000), + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.lightGrey, + ), + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 15, + backgroundImage: + booking.profilePic != null && + booking + .profilePic! + .isNotEmpty + ? NetworkImage( + booking.profilePic!, + ) + : null, + child: + booking.profilePic == null || + booking + .profilePic! + .isEmpty + ? const Icon( + Icons.person, + size: 20, + ) + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + booking.vendorName ?? + 'No data', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w600, + fontSize: 11, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color( + 0xFF353434, + ), + ), + overflow: + TextOverflow.ellipsis, + ), + ), + const Icon( + Icons.star, + color: Colors.orange, + size: 15, + ), + const SizedBox(width: 4), + Text( + '${booking.isRated ?? '0.0'}', + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: + 'SF UI Display', + fontWeight: + FontWeight.w800, + fontSize: 10, + height: 12.5 / 8.82, + letterSpacing: -0.31, + ), + ), + ], + ), + const SizedBox(height: 7), + Text( + booking.categoryName ?? + 'No category', + style: const TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: 11.84, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color(0xFF717171), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.cancelDate != null + ? _formatDate(booking.cancelDate) + : 'No date', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.cancelDate != null + ? DateFormat('h:mm a').format( + DateTime.parse(booking.cancelDate), + ) + : 'No time', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Working hours :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.workingHours ?? 'No hours', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + }, + ), + ), + ); + } + + String _formatDate(String dateString) { + try { + return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString)); + } catch (e) { + return dateString; // Fallback: return the original string if parsing fails + } + } +} diff --git a/lib/view/user_main_screens/history_screen/completed_screen.dart b/lib/view/user_main_screens/history_screen/completed_screen.dart new file mode 100644 index 0000000..8085ed2 --- /dev/null +++ b/lib/view/user_main_screens/history_screen/completed_screen.dart @@ -0,0 +1,446 @@ + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:intl/intl.dart'; +// Make sure to import your custom colors and dialog widget +// Assuming this exists + +class CompletedPage extends ConsumerWidget { + // Changed to ConsumerWidget + const CompletedPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Added WidgetRef parameter + final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: bookingAsyncValue.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text('Error: $err')), + data: (bookings) { + // Filter only canceled bookings (status == 3) + final canceledBookings = + bookings.where((booking) => booking.status == 1).toList(); + + if (canceledBookings.isEmpty) { + return const Center( + child: Text( + 'No canceled bookings found', + style: TextStyle(fontSize: 16), + ), + ); + } + + return ListView.builder( + itemCount: canceledBookings.length, + shrinkWrap: false, + itemBuilder: (context, index) { + final booking = canceledBookings[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Top Row: ID and "View order" + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + + const Divider(), + + /// Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + booking.images1.isNotEmpty + ? Image.network( + booking.images1[0], + width: 100, + height: 110, + fit: BoxFit.cover, + loadingBuilder: ( + context, + child, + loadingProgress, + ) { + if (loadingProgress == null) { + return child; + } else { + return Center( + child: CircularProgressIndicator( + value: + loadingProgress + .expectedTotalBytes != + null + ? loadingProgress + .cumulativeBytesLoaded / + (loadingProgress + .expectedTotalBytes ?? + 1) + : null, + ), + ); + } + }, + errorBuilder: ( + context, + error, + stackTrace, + ) { + return const Icon( + Icons.error, + size: 100, + ); + }, + ) + : const Icon( + Icons.image_not_supported, + size: 100, + ), + ), + ), + + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.vendorName ?? 'No vendor name', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + booking.serviceName ?? + 'No service name', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 12.52 / 13.91, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + + const SizedBox(width: 10), + Container( + width: 70, + height: 25, + decoration: BoxDecoration( + color: const Color(0xffFFEEDB), + borderRadius: BorderRadius.circular( + 6.05, + ), + ), + child: Center( + child: Text( + 'Completed', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: const Color(0xffFF8515), + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? _formatDate( + booking.serviceDate, + ) + : 'No date', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? DateFormat('h:mm a').format( + DateTime.parse( + booking.serviceDate, + ), + ) + : 'No time', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "Mobile number : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(width: 7), + Text( + booking.mobileNumber, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + children: [ + Text( + "E-mail ID :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(width: 7), + Text( + booking.email, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + children: [ + Text( + "Address : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(width: 7), + Text( + booking.address, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + SizedBox(height: 10), + Text( + "Message : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(height: 7), + Text( + booking.message, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + }, + ), + ), + ); + } + + String _formatDate(String dateString) { + try { + return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString)); + } catch (e) { + return dateString; // Fallback: return the original string if parsing fails + } + } +} diff --git a/lib/view/user_main_screens/history_screen/enquriy_page.dart b/lib/view/user_main_screens/history_screen/enquriy_page.dart new file mode 100644 index 0000000..091b284 --- /dev/null +++ b/lib/view/user_main_screens/history_screen/enquriy_page.dart @@ -0,0 +1,376 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fluttertoast/fluttertoast.dart'; + +class EnquriyPage extends ConsumerStatefulWidget { + const EnquriyPage({super.key}); + + @override + ConsumerState createState() => _EnquriyPageState(); +} + +class _EnquriyPageState extends ConsumerState { + int? expandedIndex; + + @override + Widget build(BuildContext context) { + final enquiryListAsync = ref.watch(enquriylistProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: enquiryListAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + data: (enquiries) => SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: List.generate(enquiries.length, (index) { + final enquiry = enquiries[index]; + final isExpanded = expandedIndex == index; + + return SizedBox( + width: double.infinity, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: AppColors.secondprimary, + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x66AEAEAE), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: (enquiry.images1.isEmpty) + ? Container( + width: 89, + height: 64, + color: Colors.grey[300], + child: Icon( + Icons.image, + color: Colors.grey[600], + ), + ) + : Image.network( + enquiry.images1.first, + width: 89, + height: 64, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + width: 89, + height: 64, + color: Colors.grey[300], + child: Icon( + Icons.broken_image, + color: Colors.grey[600], + ), + ), + ), + ), + + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + enquiry.vendorName ?? 'vendorName', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + height: 18.14 / 16, + letterSpacing: 0.16, + color: Colors.black, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + setState(() { + expandedIndex = isExpanded + ? null + : index; + }); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + enquiry.serviceName, + textAlign: TextAlign.left, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 16.11, + height: 17.3 / 16.11, + letterSpacing: 0.1611, + color: Colors.black, + ), + ), + ), + const SizedBox(width: 4), + Flex( + direction: Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isExpanded + ? "View Less" + : "View More", + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13.15, + height: 5.71 / 13.15, + color: Color(0xFFFF3D00), + ), + ), + const SizedBox(width: 5), + Icon( + isExpanded + ? Icons.keyboard_arrow_up + : Icons + .keyboard_arrow_down, + color: const Color( + 0xffFF3D00, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + if (isExpanded) + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.43), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 0.82, + ), + boxShadow: const [ + BoxShadow( + color: Color(0xA9A9A940), + blurRadius: 3.29, + offset: Offset(0, 0.82), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text.rich( + TextSpan( + children: [ + const TextSpan( + text: "Name : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 13, + height: 1.239, + ), + ), + TextSpan( + text: enquiry.name ?? 'No Name', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.239, + color: Color(0xFF373636), + ), + ), + ], + ), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: AppColors.red, + size: 20, + ), + onPressed: () async { + final shouldDelete = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Enquiry'), + content: const Text( + 'Are you sure you want to delete this enquiry?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of( + context, + ).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => + Navigator.of(context).pop(true), + child: const Text('Delete'), + ), + ], + ), + ); + + if (shouldDelete == true) { + Fluttertoast.showToast( + msg: 'Deleting enquiry...', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.grey, + textColor: Colors.white, + ); + + try { + final success = await ref.read( + enquriydeleteProvider( + enquiry.id.toString(), + ).future, + ); + + if (success) { + // Reset expanded index and refresh the list + setState(() { + expandedIndex = null; + }); + + await Future.delayed( + const Duration(milliseconds: 300), + ); + + ref.invalidate(enquriylistProvider); + + Fluttertoast.showToast( + msg: 'Enquiry deleted successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + } else { + Fluttertoast.showToast( + msg: 'Failed to delete enquiry', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } catch (e) { + Fluttertoast.showToast( + msg: 'Error: ${e.toString()}', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + }, + ), + ], + ), + const SizedBox(height: 8), + _infoText( + "Mobile number : ", + enquiry.mobile ?? 'No Mobile', + ), + const SizedBox(height: 13), + _infoText( + "E-mail Id : ", + enquiry.email ?? 'No Email', + ), + const SizedBox(height: 13), + _infoText( + "Message : ", + enquiry.message ?? 'No Message', + ), + ], + ), + ), + ], + ), + ); + }), + ), + ), + ), + ); + } + + Widget _infoText(String label, String value) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: label, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 13, + height: 1.239, + ), + ), + TextSpan( + text: value, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.239, + color: Color(0xFF373636), + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/user_main_screens/history_screen/history_main_contoller.dart b/lib/view/user_main_screens/history_screen/history_main_contoller.dart new file mode 100644 index 0000000..92be0f2 --- /dev/null +++ b/lib/view/user_main_screens/history_screen/history_main_contoller.dart @@ -0,0 +1,175 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/Service_Booking.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/canceled_page.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/completed_screen.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/enquriy_page.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/payment_deatils.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:flutter/material.dart'; + +class HistoryScreen extends StatefulWidget { + final int? initialTabIndex; + + const HistoryScreen({super.key, this.initialTabIndex}); + + @override + State createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + int _currentIndex = 0; + TabController? _tabController; + + @override + bool get wantKeepAlive => true; + + final List tabTitles = [ + "Service Booking", // Index 0 + "Enquiry list", // Index 1 + "Payment Details", // Index 2 + "Completed Booking", // Index 3 + "Cancelation Booking", // Index 4 + ]; + + @override + void initState() { + super.initState(); + + // Set initial tab index from widget parameter + if (widget.initialTabIndex != null && + widget.initialTabIndex! >= 0 && + widget.initialTabIndex! < tabTitles.length) { + _currentIndex = widget.initialTabIndex!; + } + + // Initialize TabController with the correct initial index + _tabController = TabController( + length: tabTitles.length, + vsync: this, + initialIndex: _currentIndex, + ); + + _tabController!.addListener(() { + if (_tabController!.indexIsChanging) { + setState(() { + _currentIndex = _tabController!.index; + }); + } + }); + } + + @override + void dispose() { + _tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + if (_tabController == null) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return PopScope( + onPopInvoked: (didPop) { + if (didPop) { + final indexController = InheritedIndexController.of(context); + indexController?.changeIndex(0); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(150.0), + child: Column( + children: [ + AppBar( + backgroundColor: Colors.white, + elevation: 0, + title: const Text( + "History", + style: TextStyle( + fontFamily: 'Inter', + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + centerTitle: true, + ), + Container( + margin: const EdgeInsets.only(top: 16.0), + alignment: Alignment.centerLeft, + height: 65, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TabBar( + controller: _tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + indicatorColor: Colors.transparent, + labelPadding: const EdgeInsets.symmetric(horizontal: 8.0), + tabs: List.generate( + tabTitles.length, + (index) => _buildCustomTab( + tabTitles[index], + index == _currentIndex, + ), + ), + onTap: (index) { + setState(() { + _currentIndex = index; + }); + }, + ), + ), + ), + ], + ), + ), + body: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + ServiceBooking(), // Index 0 + EnquriyPage(), // Index 1 + PaymentDetails(), // Index 2 + CompletedPage(), // Index 3 + CanceledPage(), // Index 4 + ], + ), + ), + ); + } + + Widget _buildCustomTab(String text, bool isSelected) { + return Tab( + height: 53, + child: Container( + width: 180, + height: 45, + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : AppColors.secondprimary, + border: Border.all(color: const Color(0xFFDBDBDB)), + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 15, + height: 1.0, + letterSpacing: 0.5, + color: isSelected ? AppColors.secondprimary : AppColors.lightgray, + ), + ), + ), + ); + } +} diff --git a/lib/view/user_main_screens/history_screen/payment_deatils.dart b/lib/view/user_main_screens/history_screen/payment_deatils.dart new file mode 100644 index 0000000..4f4f024 --- /dev/null +++ b/lib/view/user_main_screens/history_screen/payment_deatils.dart @@ -0,0 +1,448 @@ + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:intl/intl.dart'; + +class PaymentDetails extends ConsumerStatefulWidget { + const PaymentDetails({super.key}); + + @override + ConsumerState createState() => _PaymentDetailsState(); +} + +class _PaymentDetailsState extends ConsumerState { + List _localPayments = []; + + @override + Widget build(BuildContext context) { + final paymentAsync = ref.watch(paymentdetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: paymentAsync.when( + data: (payments) { + final filteredPayments = payments.where((p) => p.type == 1).toList(); + + // Initialize local copy only once + if (_localPayments.isEmpty) { + _localPayments = List.from(filteredPayments); + } + + if (_localPayments.isEmpty) { + return const Center(child: Text("No payment history found.")); + } + + return RefreshIndicator( + onRefresh: () async { + final refreshed = await ref.refresh( + paymentdetailsProvider.future, + ); + setState(() { + _localPayments = refreshed.where((p) => p.type == 2).toList(); + }); + }, + child: ListView.builder( + itemCount: _localPayments.length, + itemBuilder: (context, index) { + final payment = _localPayments[index]; + return Dismissible( + key: Key(payment.id.toString()), + direction: DismissDirection.endToStart, + background: Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(20), + ), + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(Icons.delete, color: Colors.white), + ), + confirmDismiss: (direction) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Confirm Delete"), + content: const Text( + "Are you sure you want to delete this payment record?", + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("CANCEL"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text( + "DELETE", + style: TextStyle(color: Colors.red), + ), + ), + ], + ); + }, + ); + }, + onDismissed: (direction) async { + Fluttertoast.showToast( + msg: 'Deleting payment...', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.grey, + textColor: Colors.white, + ); + + final success = await ref.read( + paymentdeleteProvider(payment.id.toString()).future, + ); + + if (success) { + setState(() { + _localPayments.removeAt(index); + }); + + Fluttertoast.showToast( + msg: '${payment.planName} deleted successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + } else { + Fluttertoast.showToast( + msg: 'Failed to delete payment', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + }, + + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFFCFCFCF), + width: 1, + ), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + width: 130, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF334E95), + borderRadius: BorderRadius.circular(12.86), + border: Border.all( + color: Colors.white, + width: 0.43, + ), + boxShadow: [ + BoxShadow( + color: const Color(0x40828282), + blurRadius: 1.71, + offset: const Offset(0, 2.14), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + payment.planName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.0, + letterSpacing: 0.1371, + color: Colors.white, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 15), + Row( + children: [ + Container( + width: 6.86, + height: 6.86, + decoration: const BoxDecoration( + color: Color(0xFFF9E369), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _formatDuration(payment.duration), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 12, + height: 1.0, + letterSpacing: 0.0771, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Container( + width: 6.86, + height: 6.86, + decoration: const BoxDecoration( + color: Color(0xFFF9E369), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + payment.description, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 12, + height: 1.0, + letterSpacing: 0.0771, + color: Colors.white, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const Divider(color: Colors.white), + Center( + child: Text( + _formatPrice(payment.price), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + payment.planName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 19.9, + height: 1.0, + letterSpacing: 0.199, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Start :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + DateFormat('MMM d, y').format( + DateTime.parse( + payment.createdDate, + ), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "End :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + DateFormat('MMM d, y').format( + DateTime.parse(payment.endDate), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Time :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + DateFormat('hh:mm a').format( + DateTime.parse( + payment.createdDate, + ), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Payment :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + _formatPrice(payment.price), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text("Error: ${e.toString()}")), + ), + ); + } + + static String _formatDuration(int days) { + if (days == 30) return '1 Month'; + if (days == 60) return '2 Months'; + if (days == 90) return '3 Months'; + if (days == 180) return '6 Months'; + if (days == 365) return '1 Year'; + return '$days Days'; + } + + static String _formatPrice(String price) { + if (price.startsWith('Rs.')) { + return price; + } + return 'Rs.$price'; + } +} diff --git a/lib/view/user_main_screens/home_screen.dart b/lib/view/user_main_screens/home_screen.dart new file mode 100644 index 0000000..d7e0fd5 --- /dev/null +++ b/lib/view/user_main_screens/home_screen.dart @@ -0,0 +1,2480 @@ +// Fixed All Services Tab Content Widget - No Loading States with Search Functionality + +// ignore_for_file: unused_result + +import 'dart:math'; + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/user_flow_drawer.dart'; +import 'package:bookmywages/model/Categories_model.dart'; +import 'package:bookmywages/model/cancel_booking.dart'; +import 'package:bookmywages/model/most_popular_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/user_main_screens/history_screen/Service_Booking.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:carousel_slider/carousel_slider.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:intl/intl.dart'; + +import '../../../consts_widgets/app_assets.dart'; + +class AllServicesTabContent extends ConsumerWidget { + final String? searchQuery; + + const AllServicesTabContent({super.key, this.searchQuery}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Safe access to provider data with error handling + final popularServicesAsync = ref.watch(mostPopularProvider("0")); + + // Handle different states safely + final allServices = popularServicesAsync.when( + data: (services) => services ?? [], + loading: () => [], + error: (error, stack) { + // Log error but don't show it to user, just return empty list + debugPrint('Error loading popular services: $error'); + return []; + }, + ); + + // Filter services based on search query + final services = searchQuery == null || searchQuery!.isEmpty + ? allServices + : allServices.where((service) { + final serviceName = (service.serviceName ?? '').toLowerCase(); + final query = searchQuery!.toLowerCase(); + return serviceName.contains(query); + }).toList(); + + // Always show content, never loading + if (services.isEmpty) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Text( + searchQuery != null && searchQuery!.isNotEmpty + ? "No services found for '$searchQuery'" + : "No services available", + ), + ), + ); + } + + final int itemCount = services.length > 4 ? 4 : services.length; + final int rowsNeeded = (itemCount + 1) ~/ 2; + final double gridHeight = rowsNeeded * 250.0; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: gridHeight, + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: itemCount, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 0.7, + ), + itemBuilder: (context, index) { + final service = services[index]; + return ServiceCard(service: service); + }, + ), + ), + ); + } +} + +class HomeScreen extends ConsumerStatefulWidget { + const HomeScreen({super.key}); + + @override + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState + with WidgetsBindingObserver, TickerProviderStateMixin { + final CarouselSliderController _controller = CarouselSliderController(); + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + int? selectedIndex; + late TabController _tabController; + List tabTitles = []; + List items = []; + bool _isFirstLoad = true; + bool _tabsInitialized = false; + String _searchQuery = ''; + + // Filter variables + String? selectedCategory; + String? selectedSubcategory; + String? selectedType; + String? selectedSubcategoryId; + String? selectedTypeValue; + List categories = []; + List subcategories = []; + + String? selectedCategoryId; + int defaultTabIndex = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _isFirstLoad = false; + + // Add search listener + _searchController.addListener(() { + if (mounted) { + setState(() { + _searchQuery = _searchController.text; + }); + } + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Removed problematic refresh calls + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + // Removed problematic refresh calls + } + + final GlobalKey _scaffoldKey = GlobalKey(); + + void _initializeTabController(List categories) { + if (categories.isEmpty || _tabsInitialized) return; + + try { + tabTitles = + ["All"] + + categories.map((cat) => cat.name as String).toList(); + selectedCategoryId = "0"; + + _tabController = TabController( + length: categories.length + 1, + vsync: this, + initialIndex: defaultTabIndex, + ); + + _tabController.addListener(() { + if (mounted) { + setState(() {}); + } + }); + + _tabsInitialized = true; + } catch (e) { + debugPrint('Error initializing tab controller: $e'); + } + } + + @override + void dispose() { + if (_tabsInitialized) { + _tabController.dispose(); + } + _searchController.dispose(); + WidgetsBinding.instance.removeObserver(this); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Safe access to all providers with proper error handling + final bannerAsyncValue = ref.watch(bannerListProvider); + final categoryAsync = ref.watch(categoryListProvider); + final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider); + final expiredPlan = ref.watch(expiredPlanProvider); + final profileData = ref.watch(profilegetuserProvider); + final indexController = InheritedIndexController.of(context); + final screenSize = MediaQuery.of(context).size; + final double verticalSpacing = screenSize.height * 0.03; + + // Safe data extraction with error handling + final banners = bannerAsyncValue.when( + data: (data) => data ?? [], + loading: () => [], + error: (_, __) => [], + ); + + final categoriesData = categoryAsync.when( + data: (data) => data ?? [], + loading: () => [], + error: (_, __) => [], + ); + + final bookings = bookingAsyncValue.when( + data: (data) => data ?? [], + loading: () => [], + error: (_, __) => [], + ); + + final plan = expiredPlan.when( + data: (data) => data, + loading: () => null, + error: (_, __) => null, + ); + + final profiles = profileData.when( + data: (data) => data ?? [], + loading: () => [], + error: (_, __) => [], + ); + + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: AppColors.secondprimary, + key: _scaffoldKey, + drawer: DrawerMenu( + userName: profiles.isNotEmpty + ? (profiles.first.name ?? "User") + : "User", + userImage: profiles.isNotEmpty + ? (profiles.first.profilePic1 ?? "") + : "", + ), + body: SafeArea( + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Header Section + Padding( + padding: const EdgeInsets.only(left: 16, right: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + // Safe scaffold access without geometry calls + if (mounted) { + try { + final scaffoldState = _scaffoldKey.currentState; + if (scaffoldState != null && + !scaffoldState.isDrawerOpen) { + scaffoldState.openDrawer(); + } + } catch (e) { + debugPrint('Error opening drawer: $e'); + } + } + }, + child: Image.asset(AppAssets.menu, height: 40), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Welcome', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.0, + letterSpacing: 0.2372, + color: Colors.black, + ), + ), + SizedBox(height: 5), + Text( + '👋 ${profiles.isNotEmpty ? (profiles.first.name ?? "User") : "User"}', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13.48, + height: 1.5, + letterSpacing: 0.2696, + color: Colors.black, + ), + ), + ], + ), + ), + const Spacer(), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.lightGrey, + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon( + Icons.notifications_none, + color: Colors.black, + ), + onPressed: () {}, + ), + ), + ], + ), + ), + SizedBox(height: 20), + + // Banner Section - Always show content + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 5, + ), + child: banners.isNotEmpty + ? CarouselSlider( + options: CarouselOptions( + height: 180, + autoPlay: false, + enlargeCenterPage: true, + viewportFraction: 1.0, + autoPlayCurve: Curves.fastOutSlowIn, + enableInfiniteScroll: true, + ), + carouselController: _controller, + items: banners.map((banner) { + return Builder( + builder: (BuildContext context) { + return Container( + height: 180, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + image: DecorationImage( + image: CachedNetworkImageProvider( + banner.documentUrl ?? '', + ), + fit: BoxFit.cover, + onError: (exception, stackTrace) { + debugPrint( + 'Banner image error: $exception', + ); + }, + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + // Left Arrow + Transform.translate( + offset: const Offset(-15, 0), + child: Padding( + padding: const EdgeInsets.only( + left: 8.0, + ), + child: GestureDetector( + onTap: () => + _controller.previousPage(), + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.2), + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Center( + child: Image.asset( + AppAssets.arrowbutton, + width: 50, + height: 50, + color: AppColors.thridprimary, + ), + ), + ), + ), + ), + ), + // Right Arrow + Transform.translate( + offset: const Offset(15, 0), + child: Padding( + padding: const EdgeInsets.only( + right: 8.0, + ), + child: GestureDetector( + onTap: () => _controller.nextPage(), + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.2), + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Center( + child: Transform.rotate( + angle: 3.14, + child: Image.asset( + AppAssets.arrowbutton, + width: 50, + height: 50, + color: AppColors.thridprimary, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }).toList(), + ) + : Container( + height: 180, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.grey[300], + ), + child: Center(child: Text('No banners available')), + ), + ), + SizedBox(height: 20), + + // Search Section with Enhanced Search Functionality + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Container( + width: 323.63, + height: 60.3, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.08), + border: Border.all( + color: const Color(0xFFAEAEAE), + width: 1.01, + ), + ), + child: TextFormField( + controller: _searchController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + border: InputBorder.none, + hintText: 'Search your service', + hintStyle: TextStyle(color: Colors.grey[600]), + prefixIcon: Icon( + Icons.search_sharp, + color: Colors.grey[600], + ), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + color: Colors.grey[600], + ), + onPressed: () { + _searchController.clear(); + if (mounted) { + setState(() { + _searchQuery = ''; + }); + } + }, + ) + : null, + ), + style: const TextStyle( + fontSize: 16, + color: Colors.black, + ), + onChanged: (value) { + if (mounted) { + setState(() { + _searchQuery = value; + }); + } + }, + ), + ), + ), + SizedBox(width: 5), + Expanded( + flex: 1, + child: Container( + width: 62.31, + height: 60.30, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.08), + border: Border.all( + color: const Color(0xFFAEAEAE), + width: 1.01, + ), + ), + child: Center( + child: IconButton( + onPressed: () { + // Add small delay to avoid scaffold geometry issues + Future.delayed(Duration(milliseconds: 50), () { + if (mounted) { + _showCommanFilterBottomSheet(context); + } + }); + }, + icon: Image.asset( + AppAssets.filtericon, + height: 25, + width: 25, + color: const Color(0xff797777), + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: 20), + + // Free/Paid Service Buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Get.toNamed( + '${RouterConts.listservice}/0', // category id = 0 + arguments: { + 'service': + 'free', // service type = 1 (manual/paid) + 'subcategoryId': + null, // or specific subcategory id if needed + }, + ); + }, + child: Container( + width: 181, + height: 60, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Color(0xFFC7C7C7), + width: 1, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + AppAssets.free, + width: 46, + height: 46, + ), + SizedBox(width: 10), + Text( + "Free", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 22, + height: 1.7, + letterSpacing: 0.22, + color: Color(0xFF524F4F), + ), + ), + ], + ), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: GestureDetector( + onTap: () { + Get.toNamed( + '${RouterConts.listservice}/0', // category id = 0 + arguments: { + 'service': '1', // service type = 1 (manual/paid) + 'subcategoryId': + null, // or specific subcategory id if needed + }, + ); + }, + child: Container( + width: 181, + height: 60, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Color(0xFFC7C7C7), + width: 1, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + AppAssets.paid, + width: 46, + height: 46, + ), + SizedBox(width: 10), + Text( + "Paid", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 22, + height: 1.7, + letterSpacing: 0.22, + color: Color(0xFF524F4F), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: verticalSpacing), + + // Categories Section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Categories', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 18, + height: 27.59 / 24, + letterSpacing: 1, + color: AppColors.thridprimary, + ), + ), + GestureDetector( + onTap: () { + Get.offAllNamed(RouterConts.categorypage, arguments: 2); + }, + child: Text( + 'View more', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + height: 25.82 / 16, + letterSpacing: 1, + color: AppColors.thridprimary, + ), + ), + ), + ], + ), + ), + + // Categories Grid - Always show content + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: categoriesData.isNotEmpty + ? GridView.count( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 20, + childAspectRatio: 0.9, + children: List.generate( + categoriesData.length > 6 ? 6 : categoriesData.length, + (index) { + var item = categoriesData[index]; + bool isSelected = selectedIndex == index; + + Widget imageWidget; + try { + if (item.name.toLowerCase() == 'plumbing') { + imageWidget = Image.network( + item.getIconUrl(), + height: 50, + width: 50, + errorBuilder: (context, error, stackTrace) => + Icon(Icons.error, size: 50), + ); + } else if (item.name.toLowerCase().contains( + 'electrical', + )) { + imageWidget = Image.network( + item.getIconUrl(), + height: 50, + width: 50, + errorBuilder: (context, error, stackTrace) => + Icon(Icons.error, size: 50), + ); + } else { + imageWidget = Image.network( + item.getImageUrl(), + height: 50, + width: 50, + errorBuilder: (context, error, stackTrace) => + Image.network( + item.iconUrl ?? '', + height: 50, + width: 50, + errorBuilder: + (context, error, stackTrace) => + Icon(Icons.image, size: 50), + ), + ); + } + } catch (e) { + imageWidget = Icon(Icons.category, size: 50); + } + + return GestureDetector( + onTap: () { + if (mounted) { + setState(() { + selectedIndex = index; + }); + } + Future.delayed(Duration(milliseconds: 300), () { + Get.toNamed( + RouterConts.listservice, + arguments: { + 'id': item.id, // Pass ID in arguments + 'subcategoryId': null, + 'service': '0', + }, + ); + }); + }, + child: Container( + decoration: BoxDecoration( + color: Color(0xFFE3E3E3), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + offset: Offset(2, 1), + blurRadius: 3.5, + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(isSelected ? 8 : 0), + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + padding: EdgeInsets.all( + isSelected ? 8 : 16, + ), + decoration: BoxDecoration( + color: isSelected + ? Color(0xFF52CC40) + : Color(0xffFAFAFA), + borderRadius: BorderRadius.circular(15), + ), + child: AnimatedRotation( + turns: isSelected ? -10 / 360 : 0, + duration: Duration(milliseconds: 300), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + imageWidget, + Padding( + padding: const EdgeInsets.only( + top: 6, + ), + child: Text( + item.name ?? 'Category', + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w900, + fontSize: 10, + color: Colors.black, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ) + : SizedBox( + height: 200, + child: Center(child: Text('No categories available')), + ), + ), + + SizedBox(height: 20), + + // Expired Plan Section - Show only if plan exists + if (plan != null && plan.endDate != null && plan.planName != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: AppColors.lightBlue, + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Subscription plan', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 24, + height: 27.59 / 24, + letterSpacing: 1.0, + color: Color(0xFF9C34C2), + ), + ), + GestureDetector( + onTap: () async { + try { + Get.offAllNamed( + RouterConts.history, + arguments: { + 'historyTab': 2, // Enquiry list tab + }, + ); + } catch (e) { + debugPrint('Navigation error: $e'); + } + }, + child: Text( + 'View More', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 15, + color: Color(0xFF534E4E), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Image.asset( + AppAssets.subscription, + width: 60, + height: 60, + ), + const SizedBox(width: 16), + Flexible( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Your ', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFF585454), + fontWeight: FontWeight.w400, + fontSize: 18, + height: 1.86, + letterSpacing: 0.1957, + ), + ), + TextSpan( + text: plan.planName ?? 'Subscription', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFFFF0000), + fontWeight: FontWeight.w700, + fontSize: 18, + height: 1.86, + letterSpacing: 0.1957, + ), + ), + TextSpan( + text: + ' subscription plan was expired on ', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFF585454), + fontWeight: FontWeight.w400, + fontSize: 18, + height: 1.86, + letterSpacing: 0.1957, + ), + ), + TextSpan( + text: _formatDate(plan.endDate), + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFFFF0000), + fontWeight: FontWeight.w700, + fontSize: 18, + letterSpacing: 0.1957, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Start Date: ${plan.createdDate?.split(' ').first ?? 'Not available'}', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14, + height: 13 / 14, + letterSpacing: 0.14, + color: Color(0xFF4F4F4F), + ), + ), + Text( + 'End Date: ${plan.endDate ?? 'Not available'}', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14, + height: 13 / 14, + letterSpacing: 0.14, + color: Color(0xFF4F4F4F), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () { + Get.offAllNamed( + RouterConts.packageList, + arguments: 1, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + child: const Text( + 'Renewal', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ], + ), + ), + ), + ), + + SizedBox(height: 20), + + // Your Booking Section - Always show content + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Only show header if there are bookings + if (bookings.isNotEmpty) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Your Booking', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () { + Get.offAllNamed( + RouterConts.history, + arguments: { + 'historyTab': 0, // Enquiry list tab + }, + ); + }, + child: Text( + 'View more', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 40), + ], + + // Booking list - Always show without loading + ListView.builder( + itemCount: bookings.isEmpty ? 0 : min(3, bookings.length), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final booking = bookings[index]; + return _buildBookingCard(context, booking, ref); + }, + ), + ], + ), + ), + + SizedBox(height: 20), + + // Most Popular Service Section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Most popular service', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 18, + height: 27.59 / 24, + letterSpacing: 1, + color: AppColors.thridprimary, + ), + ), + GestureDetector( + onTap: () { + Get.toNamed(RouterConts.mostpopluarserviceviewall); + }, + child: Text( + 'View more', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + height: 25.82 / 16, + letterSpacing: 1, + color: AppColors.thridprimary, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 30), + + // Dynamic TabBar and TabBarView - Always show content with search functionality + categoriesData.isNotEmpty + ? Builder( + builder: (context) { + // Initialize tab controller if not already done + if (!_tabsInitialized && categoriesData.isNotEmpty) { + // Initialize immediately without post-frame callback + _initializeTabController(categoriesData); + } + + if (!_tabsInitialized) { + return Center(child: Text("Loading categories...")); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: Colors.transparent, + child: TabBar( + tabAlignment: TabAlignment.start, + controller: _tabController, + isScrollable: true, + indicatorColor: Colors.transparent, + labelColor: Colors.white, + unselectedLabelColor: Colors.black, + dividerColor: Colors.transparent, + onTap: (index) { + if (mounted) { + setState(() { + _tabController.animateTo(index); + if (index == 0) { + selectedCategoryId = "0"; + } else { + selectedCategoryId = + categoriesData[index - 1].id + .toString(); + } + }); + } + }, + tabs: List.generate(categoriesData.length + 1, ( + index, + ) { + bool isSelected = + _tabController.index == index; + return Tab( + child: Container( + height: 48, + decoration: BoxDecoration( + color: isSelected + ? Color(0xFF0066FF) + : Colors.white, + border: Border.all( + color: isSelected + ? Color(0xFF0066FF) + : Color(0xFFB7B7B7), + width: isSelected ? 0 : 1, + ), + borderRadius: BorderRadius.circular(38), + ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 40, + ), + child: Center( + child: Text( + index == 0 + ? "All" + : categoriesData[index - 1] + .name ?? + "Category", + style: TextStyle( + color: isSelected + ? Colors.white + : Colors.black, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + }), + ), + ), + const SizedBox(height: 20), + IndexedStack( + index: _tabController.index, + sizing: StackFit.loose, + children: List.generate( + categoriesData.length + 1, + (index) => index == 0 + ? AllServicesTabContent( + searchQuery: _searchQuery, + ) + : ServiceTabContent( + category: categoriesData[index - 1], + searchQuery: _searchQuery, + ), + ), + ), + ], + ); + }, + ) + : Center(child: Text('No categories available')), + + SizedBox(height: 10), + ], + ), + ), + ), + ); + } + + // Helper method to format date + String _formatDate(dynamic dateString) { + try { + final date = DateTime.parse(dateString.toString()); + return DateFormat('MMMM dd').format(date); + } catch (_) { + return dateString?.toString() ?? 'Not available'; + } + } + + // Helper method to build booking card + Widget _buildBookingCard( + BuildContext context, + dynamic booking, + WidgetRef ref, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Row: ID and "View order" + Padding( + padding: const EdgeInsets.only(right: 12, left: 12, top: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id ?? 'N/A'}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + GestureDetector( + onTap: () { + Get.toNamed( + RouterConts.detailserivce, + arguments: booking.serviceId, + ); + }, + child: Text( + 'View order', + style: TextStyle( + color: Colors.blue.shade600, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + const Divider(), + + // Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (booking.images1 != null && + booking.images1!.isNotEmpty) + ? Image.network( + booking.images1![0], + width: 100, + height: 110, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 100, + height: 110, + color: Colors.grey[300], + child: const Icon(Icons.error, size: 50), + ); + }, + ) + : Container( + width: 100, + height: 110, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + size: 50, + ), + ), + ), + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.vendorName ?? 'Vendor', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + booking.serviceName ?? 'Service', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 1.3, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container( + width: booking.status == 3 + ? 70 + : booking.status == 1 + ? 85 + : 77.84, + height: booking.status == 3 + ? 25 + : booking.status == 1 + ? 25 + : 27.96, + decoration: BoxDecoration( + color: booking.status == 3 + ? const Color(0xFFFFEEEE) + : booking.status == 1 + ? const Color(0xFFE6F7E6) + : const Color(0xFFDAE9FF), + borderRadius: BorderRadius.circular(6.05), + ), + child: Center( + child: Text( + booking.status == 3 + ? 'Cancel' + : booking.status == 1 + ? 'Scheduled' + : 'Pending', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: booking.status == 3 + ? const Color(0xFFFF0000) + : booking.status == 1 + ? const Color(0xFF2E8B57) + : const Color(0xFF0066FF), + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 15, + backgroundImage: + booking.profilePic != null && + booking.profilePic!.isNotEmpty + ? NetworkImage(booking.profilePic.toString()) + : null, + child: + booking.profilePic == null || + booking.profilePic!.isEmpty + ? const Icon(Icons.person, size: 20) + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + booking.vendorName ?? 'no data', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w600, + fontSize: 11, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color(0xFF353434), + ), + overflow: TextOverflow.visible, + ), + ), + const Icon( + Icons.star, + color: Colors.orange, + size: 15, + ), + const SizedBox(width: 4), + Text( + '${booking.isRated ?? '4.5'}', + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'SF UI Display', + fontWeight: FontWeight.w800, + fontSize: 10, + height: 12.5 / 8.82, + letterSpacing: -0.31, + ), + ), + ], + ), + const SizedBox(height: 7), + Text( + booking.categoryName ?? 'Category', + style: const TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: 11.84, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color(0xFF717171), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate ?? 'April 23, 2024', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceTime ?? '12:00 PM', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Working hours :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.workingHours ?? '8 hours', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + // Buttons Section + if (booking.status != 3) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + mainAxisAlignment: booking.status == 1 + ? MainAxisAlignment.end + : MainAxisAlignment.spaceBetween, + children: [ + if (booking.status != 1) + Expanded( + child: SizedBox( + height: 42, + child: ElevatedButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + width: double.maxFinite, + decoration: BoxDecoration( + color: AppColors.secondprimary, + borderRadius: BorderRadius.circular(16), + ), + child: BookingModificationDialog( + id: booking.id.toString(), + serviceId: booking.serviceId.toString(), + ), + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const FittedBox( + child: Text( + "Modification", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w800, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.289, + color: Colors.white, + ), + ), + ), + ), + ), + ), + if (booking.status != 1) const SizedBox(width: 8), + GestureDetector( + onTap: () async { + final data = CancelBookingRequest( + id: booking.id.toString(), + serviceId: booking.serviceId.toString(), + type: booking.type == 0 ? "0" : booking.type.toString(), + ); + + try { + await ref.read(cancelbookingProvider(data).future); + if (mounted) { + Fluttertoast.showToast( + msg: 'Booking cancelled successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + ref.invalidate(userbookinghistorydetailsProvider); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: 'Failed to cancel booking: $e', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Cancel Booking', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ); + } + + // Fixed Filter Bottom Sheet Implementation + void _showCommanFilterBottomSheet(BuildContext context) { + String? localSelectedCategory = selectedCategory; + String? localSelectedSubcategory = selectedSubcategory; + String? localSelectedType = selectedType; + List localSubcategories = List.from(subcategories); + List localCategories = List.from(categories); + + // Direct call without post-frame callback to avoid scaffold geometry issues + if (localCategories.isEmpty) { + final categoryRepo = ref.read(categoryRepositoryProvider); + categoryRepo + .fetchCategories(ConstsApi.catgories) + .then((loadedCategories) { + if (mounted) { + localCategories = loadedCategories; + _displayBottomSheet( + context, + localCategories, + localSubcategories, + localSelectedCategory, + localSelectedSubcategory, + localSelectedType, + ); + } + }) + .catchError((e) { + debugPrint('Error loading categories for filter: $e'); + if (mounted) { + _displayBottomSheet( + context, + [], + localSubcategories, + localSelectedCategory, + localSelectedSubcategory, + localSelectedType, + ); + } + }); + } else { + _displayBottomSheet( + context, + localCategories, + localSubcategories, + localSelectedCategory, + localSelectedSubcategory, + localSelectedType, + ); + } + } + + void _displayBottomSheet( + BuildContext context, + List localCategories, + List localSubcategories, + String? localSelectedCategory, + String? localSelectedSubcategory, + String? localSelectedType, + ) { + if (!mounted) return; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + Future loadSubcategoriesForModal(String categoryId) async { + setModalState(() { + localSubcategories = []; + }); + + final subcategoryRepo = ref.read(subcategoryRepositoryProvider); + try { + final newSubcategories = await subcategoryRepo + .fetchSubcategories(ConstsApi.subcat, categoryId); + if (context.mounted) { + setModalState(() { + localSubcategories = newSubcategories; + }); + } + } catch (e) { + debugPrint('Error loading subcategories: $e'); + if (context.mounted) { + setModalState(() { + localSubcategories = []; + }); + } + } + } + + return Container( + height: MediaQuery.of(context).size.height * 0.6, + margin: const EdgeInsets.only(top: 100), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(40), + topRight: Radius.circular(40), + ), + border: Border.all(color: const Color(0xFF858181), width: 1), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20.0, + vertical: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Filter", + style: TextStyle( + fontFamily: 'Gilroy', + fontWeight: FontWeight.w900, + fontSize: 25, + height: 1.0, + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Image.asset( + AppAssets.filtericon, + height: 25, + width: 25, + color: const Color(0xff797777), + ), + ), + ], + ), + const SizedBox(height: 20), + + // Category Dropdown + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFFDFDFD), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFD3D3D3), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: ButtonTheme( + alignedDropdown: true, + child: DropdownButton( + hint: Text("Select Category"), + value: localSelectedCategory, + isExpanded: true, + icon: Icon(Icons.arrow_drop_down), + iconSize: 24, + elevation: 16, + items: localCategories.isEmpty + ? [ + DropdownMenuItem( + value: null, + child: Text("No categories available"), + ), + ] + : localCategories.map((category) { + return DropdownMenuItem( + value: category.id.toString(), + child: Text( + category.name ?? 'Category', + ), + ); + }).toList(), + onChanged: localCategories.isEmpty + ? null + : (value) { + if (value != null) { + setModalState(() { + localSelectedCategory = value; + localSelectedSubcategory = null; + }); + loadSubcategoriesForModal(value); + } + }, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // Subcategory Dropdown + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFFDFDFD), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFD3D3D3), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: ButtonTheme( + alignedDropdown: true, + child: DropdownButton( + hint: Text("Select Subcategory"), + value: localSelectedSubcategory, + isExpanded: true, + icon: Icon(Icons.arrow_drop_down), + iconSize: 24, + elevation: 16, + items: localSubcategories.isEmpty + ? [ + DropdownMenuItem( + value: null, + child: localSelectedCategory == null + ? Text("Select a category first") + : Text( + "No subcategories available", + ), + ), + ] + : localSubcategories.map((subcategory) { + return DropdownMenuItem( + value: subcategory.id.toString(), + child: Text( + subcategory.name ?? 'Subcategory', + ), + ); + }).toList(), + onChanged: localSubcategories.isEmpty + ? null + : (value) { + setModalState(() { + localSelectedSubcategory = value; + }); + }, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // Type Dropdown + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFFDFDFD), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFD3D3D3), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: ButtonTheme( + alignedDropdown: true, + child: DropdownButton( + hint: Text("Select Type"), + value: localSelectedType, + isExpanded: true, + icon: Icon(Icons.arrow_drop_down), + iconSize: 24, + elevation: 16, + items: ["Free", "Paid"].map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), + onChanged: (value) { + setModalState(() { + localSelectedType = value; + }); + }, + ), + ), + ), + ), + + SizedBox(height: 50), + + // Save Button + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF000000), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + final updatedCategory = + localSelectedCategory ?? "0"; + final updatedSubcategory = localSelectedSubcategory; + final updatedType = localSelectedType; + + String? updatedTypeValue; + if (localSelectedType != null) { + updatedTypeValue = localSelectedType == "Paid" + ? '1' + : 'free'; + } + + if (mounted) { + setState(() { + selectedCategory = updatedCategory; + selectedSubcategory = updatedSubcategory; + selectedType = updatedType; + selectedSubcategoryId = updatedSubcategory; + selectedTypeValue = updatedTypeValue; + + if (selectedCategory == "0") { + selectedIndex = 0; + } + + categories = List.from(localCategories); + subcategories = List.from(localSubcategories); + }); + } + + debugPrint( + "Filter applied - Category: $selectedCategory, Subcategory: $selectedSubcategoryId, Type: $selectedTypeValue", + ); + + Navigator.pop(context); + + // Pass everything in arguments instead of query params + Get.toNamed( + RouterConts.listservice, + arguments: { + 'id': selectedCategory, // Move id to arguments + 'subcategoryId': selectedSubcategoryId, + 'service': selectedTypeValue, + 'sourceTab': + 0, // or whatever tab you want to maintain + }, + ); + }, + child: const Text( + 'Save', + style: TextStyle( + color: Colors.white, + fontFamily: 'Gilroy', + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } +} + +class ServiceTabContent extends ConsumerWidget { + final dynamic category; + final String? searchQuery; + + const ServiceTabContent({ + super.key, + required this.category, + this.searchQuery, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Safe access to provider data with error handling + final popularServicesAsync = ref.watch( + mostPopularProvider(category.id.toString()), + ); + + // Handle different states safely + final allServices = popularServicesAsync.when( + data: (services) => services ?? [], + loading: () => [], + error: (error, stack) { + debugPrint( + 'Error loading services for category ${category.id}: $error', + ); + return []; + }, + ); + + // Filter services based on search query + final services = searchQuery == null || searchQuery!.isEmpty + ? allServices + : allServices.where((service) { + final serviceName = (service.serviceName ?? '').toLowerCase(); + final query = searchQuery!.toLowerCase(); + return serviceName.contains(query); + }).toList(); + + // Always show content, never loading + if (services.isEmpty) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Text( + searchQuery != null && searchQuery!.isNotEmpty + ? "No services found for '$searchQuery' in this category" + : "No services available for this category", + ), + ), + ); + } + + final int itemCount = services.length > 4 ? 4 : services.length; + final int rowsNeeded = (itemCount + 1) ~/ 2; + final double gridHeight = rowsNeeded * 250.0; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + height: gridHeight, + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: itemCount, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 0.7, + ), + itemBuilder: (context, index) { + final service = services[index]; + return ServiceCard(service: service); + }, + ), + ), + ); + } +} + +// Service Card Widget - Enhanced with better error handling +class ServiceCard extends StatelessWidget { + final MostPopularModel service; + + const ServiceCard({super.key, required this.service}); + + @override + Widget build(BuildContext context) { + return Container( + width: 189, + decoration: BoxDecoration( + color: const Color(0xFFFCFAFA), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFE3E3E3), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Service image + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(19)), + child: SizedBox( + height: 120, + width: double.infinity, + child: + (service.images1 != null && + service.images1!.isNotEmpty && + _isValidCompleteUrl(service.images1![0])) + ? Image.network( + service.images1!.first, + width: double.infinity, + height: 120, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + AppAssets.cleaning, + width: double.infinity, + height: 120, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + AppAssets.cleaning, + width: double.infinity, + height: 120, + fit: BoxFit.cover, + ), + ), + ), + + // Price tag + Align( + alignment: Alignment.centerRight, + child: Container( + height: 30, + width: 80, + transform: Matrix4.translationValues(-10, -15, 0), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white, width: 1.5), + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + 'Rs.${service.amount ?? "N/A"}', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 16.86, + height: 1.0, + letterSpacing: 0.02, + color: Color(0xFFFCFAFA), + ), + ), + ), + ), + ), + ), + ), + + // Service details + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 2, + child: Text( + service.serviceName ?? 'Service', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 15.02, + height: 1.0, + letterSpacing: 0.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Expanded( + flex: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + color: Colors.orange, + size: 18, + ), + const SizedBox(width: 2), + Flexible( + child: Text( + service.averageReview?.toString() ?? '0.0', + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'SF UI Display', + fontWeight: FontWeight.w600, + fontSize: 14, + height: 1.0, + letterSpacing: -0.5, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 2, + child: Container( + width: 110, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + border: Border.all(color: AppColors.lightGrey), + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + service.profilePic1 != null && + service.profilePic1!.isNotEmpty + ? CircleAvatar( + radius: 15, + backgroundImage: NetworkImage( + service.profilePic1!, + ), + onBackgroundImageError: + (exception, stackTrace) { + debugPrint( + 'Profile image error: $exception', + ); + }, + ) + : const CircleAvatar( + radius: 15, + backgroundImage: AssetImage( + AppAssets.login, + ), + ), + const SizedBox(width: 4), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + service.vendorName ?? 'Vendor', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 11.75, + height: 1.0, + letterSpacing: 0.0, + color: Color(0xFF353434), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 5), + Text( + service.serviceName ?? 'Service', + style: const TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: 11.75, + height: 1.0, + letterSpacing: 0.0, + color: Color(0xFF717171), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () { + Get.toNamed( + RouterConts.detailserivce, + arguments: service.id, + ); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: Colors.white, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + bool _isValidCompleteUrl(String url) { + try { + final uri = Uri.parse(url); + return uri.hasScheme && + (uri.scheme == 'http' || uri.scheme == 'https') && + uri.host.isNotEmpty; + } catch (e) { + return false; + } + } +} diff --git a/lib/view/user_main_screens/image_page.dart b/lib/view/user_main_screens/image_page.dart new file mode 100644 index 0000000..67f9f04 --- /dev/null +++ b/lib/view/user_main_screens/image_page.dart @@ -0,0 +1,116 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:get/get.dart'; + +class ImagePage extends StatefulWidget { + final List mediaUrls; + + const ImagePage({super.key, required this.mediaUrls}); + + @override + State createState() => _ImagePageState(); +} + +class _ImagePageState extends State { + int selectedImageIndex = 0; // Track the currently selected image + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.secondprimary, + appBar: AppBar( + backgroundColor: AppColors.secondprimary, + leading: IconButton( + onPressed: () => Get.back(), + icon: Icon(Icons.arrow_back_ios_outlined), + ), + title: Text("Service Image"), + centerTitle: true, + ), + body: Column( + children: [ + SizedBox(height: 50), + Center( + child: ClipRRect( + borderRadius: BorderRadius.circular( + 22.94, + ), // Set border-radius to 22.94 + child: CachedNetworkImage( + imageUrl: widget.mediaUrls.isNotEmpty + ? widget.mediaUrls[selectedImageIndex] // Use selected index + : '', + width: 250, // Set width to 250 + height: 250, // Set height to 250 + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + ), + ), + SizedBox(height: 20), + CarouselSlider( + options: CarouselOptions( + height: 150, // Set height of the carousel + enlargeCenterPage: true, + enableInfiniteScroll: false, + viewportFraction: 0.3, + aspectRatio: 1.0, + ), + items: widget.mediaUrls.asMap().entries.map((entry) { + int index = entry.key; + String url = entry.value; + bool isSelected = index == selectedImageIndex; + + return GestureDetector( + onTap: () { + setState(() { + selectedImageIndex = index; // Update selected image + }); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(22.94), + border: isSelected + ? Border.all( + color: Colors.blue, + width: 3, + ) // Highlight selected image + : null, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 22.94, + ), // Set border-radius to 22.94 + child: CachedNetworkImage( + imageUrl: url, + width: 176, // Set width to 176 + height: 158, // Set height to 158 + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + errorWidget: (context, url, error) => + const Icon(Icons.error), + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } +} diff --git a/lib/view/user_main_screens/list_service_screen.dart b/lib/view/user_main_screens/list_service_screen.dart new file mode 100644 index 0000000..b70522f --- /dev/null +++ b/lib/view/user_main_screens/list_service_screen.dart @@ -0,0 +1,1787 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/model/Categories_model.dart'; +import 'package:bookmywages/model/subcategory_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; + +// Utility class for responsive sizing +class ResponsiveSize { + static double getWidthPercentage(BuildContext context, double percentage) { + return MediaQuery.of(context).size.width * percentage; + } + + static double getHeightPercentage(BuildContext context, double percentage) { + return MediaQuery.of(context).size.height * percentage; + } + + static double getFontSize(BuildContext context, double baseFontSize) { + double screenWidth = MediaQuery.of(context).size.width; + // Scale font based on screen width + if (screenWidth < 320) return baseFontSize * 0.8; + if (screenWidth < 480) return baseFontSize * 0.9; + return baseFontSize; + } +} + +class ListServiceScreen extends ConsumerStatefulWidget { + final String id; // Changed from int to String + final String service; + final String? subcategoryId; // Added subcategoryId parameter + + const ListServiceScreen({ + super.key, + required this.id, + required this.service, + this.subcategoryId, // Added subcategoryId parameter + }); + + @override + ConsumerState createState() => _ListServiceScreenState(); +} + +class _ListServiceScreenState extends ConsumerState { + int selectedIndex = 0; + String? selectedSubcategoryId; + String? selectedCategory; + String? selectedSubcategory; + String? selectedType; + List categories = []; + List subcategories = []; + String? selectedTypeValue; + + // For view more bottom sheet + int viewMoreSelectedIndex = 0; + String? tempSelectedSubcategoryId; + + // Search functionality variables + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + + @override + void initState() { + super.initState(); + // Add search listener + _searchController.addListener(() { + setState(() { + _searchQuery = _searchController.text; + }); + }); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final isPortrait = + MediaQuery.of(context).orientation == Orientation.portrait; + final itemSize = isPortrait ? size.width * 0.16 : size.width * 0.1; + + // Fetching subcategory and services + final subcategoryAsyncValue = ref.watch( + subcategoryProvider(widget.id.toString()), // Fetching subcategories + ); + + // Fetching services based on selected filters + final servicesAsyncValue = ref.watch( + serviceProvider(( + categoryId: + selectedCategory ?? + widget.id.toString(), // Use selected category or default + subcategoryId: selectedSubcategoryId, + selecttype: selectedTypeValue ?? widget.service, + )), + ); + + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: ResponsiveSize.getWidthPercentage(context, 0.04), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.02), + ), + // iOS style back button and title + Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon( + Icons.arrow_back_ios_new, + size: 20, + color: Colors.black, + ), + ), + ), + Center( + child: Text( + 'List of Service', + style: TextStyle( + fontSize: ResponsiveSize.getFontSize(context, 20), + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.05), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Container( + width: 323.63, // fixed width from your value + height: 60.3, // fixed height + // position if needed (usually use Padding or layout widget instead) + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.08), + border: Border.all( + color: const Color(0xFFAEAEAE), + width: 1.01, + ), + ), + child: TextFormField( + controller: _searchController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + border: InputBorder.none, + hintText: 'Search your service', + hintStyle: TextStyle(color: Colors.grey[600]), + prefixIcon: Icon( + Icons.search_sharp, + color: Colors.grey[600], + ), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + color: Colors.grey[600], + ), + onPressed: () { + _searchController.clear(); + setState(() { + _searchQuery = ''; + }); + }, + ) + : null, + ), + style: const TextStyle( + fontSize: 16, + color: Colors.black, + ), + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + ), + ), + ), + SizedBox(width: 5), + Expanded( + flex: 1, + child: Container( + width: 62.31, + height: 60.30, + decoration: BoxDecoration( + color: Colors.white, // #FFFFFF + borderRadius: BorderRadius.circular(15.08), + border: Border.all( + color: const Color(0xFFAEAEAE), + width: 1.01, + ), + ), + child: Center( + child: IconButton( + onPressed: () => + showCommanFilterBottomSheet(context), + icon: Image.asset( + AppAssets.filtericon, + height: 25, + width: 25, + color: const Color(0xff797777), + ), + ), + ), + ), + ), + ], + ), + + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.02), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Sub - categories', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: size.width * 0.055, // Responsive font size + height: 1.0, + letterSpacing: size.width * 0.0005, + ), + ), + GestureDetector( + onTap: () { + showViewMoreBottomSheet(context, subcategoryAsyncValue); + }, + child: Text( + 'View more', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: MediaQuery.of(context).size.width * 0.04, + height: 1.0, + letterSpacing: + MediaQuery.of(context).size.width * 0.0005, + ), + ), + ), + ], + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.04), + ), + subcategoryAsyncValue.when( + loading: () => SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.14), + child: Row( + children: List.generate( + 4, + (index) => Padding( + padding: EdgeInsets.only( + right: ResponsiveSize.getWidthPercentage( + context, + 0.05, + ), + ), + child: Column( + children: [ + Container( + height: itemSize, + width: itemSize, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + Container( + height: 12, + width: itemSize * 0.8, + color: Colors.grey.shade300, + ), + ], + ), + ), + ), + ), + ), + error: (err, stack) => Text('Error: $err'), + data: (subcategories) { + return SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.14), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: subcategories.length + 1, + itemBuilder: (context, index) { + bool isSelected = index == selectedIndex; + + if (index == 0) { + return GestureDetector( + onTap: () { + setState(() { + selectedIndex = 0; + selectedSubcategoryId = null; + selectedCategory = widget.id.toString(); + }); + }, + child: Padding( + padding: EdgeInsets.only( + right: ResponsiveSize.getWidthPercentage( + context, + 0.05, + ), + ), + child: Column( + children: [ + Container( + height: itemSize, + width: itemSize, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade100 + : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Image.asset(AppAssets.allicon), + ), + SizedBox( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + Text( + 'All', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: ResponsiveSize.getFontSize( + context, + 12, + ), + fontWeight: FontWeight.w500, + color: isSelected + ? Colors.black + : Colors.grey, + ), + ), + ], + ), + ), + ); + } + + final subcategory = subcategories[index - 1]; + return GestureDetector( + onTap: () { + setState(() { + selectedIndex = index; + selectedSubcategoryId = subcategory.id + .toString(); + }); + }, + child: Padding( + padding: EdgeInsets.only( + right: ResponsiveSize.getWidthPercentage( + context, + 0.05, + ), + ), + child: Column( + children: [ + Container( + height: itemSize, + width: itemSize, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade100 + : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Padding( + padding: EdgeInsets.all(itemSize * 0.18), + child: CachedNetworkImage( + imageUrl: subcategory.icon1, + fit: BoxFit.contain, + placeholder: (context, url) => SizedBox( + width: 20, + height: 20, + child: Icon( + Icons.category, + size: 20, + color: Colors.grey, + ), + ), + errorWidget: (context, url, error) => + Icon( + Icons.broken_image, + color: Colors.grey, + ), + ), + ), + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + SizedBox( + width: itemSize, + child: Text( + subcategory.name, + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + maxLines: 2, + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: ResponsiveSize.getFontSize( + context, + 12, + ), + height: 1.2, + color: isSelected + ? Colors.black + : Color(0xff909090), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.03), + ), + + servicesAsyncValue.when( + data: (allServices) { + // Filter services based on search query + final services = _searchQuery.isEmpty + ? allServices + : allServices.where((service) { + final serviceName = service.serviceName + .toLowerCase(); + final vendorName = service.vendorName.toLowerCase(); + final categoryName = service.categoryName + .toLowerCase(); + final vendorDisplayName = service.vendorDisplayName + .toLowerCase(); + final query = _searchQuery.toLowerCase(); + + return serviceName.contains(query) || + vendorName.contains(query) || + categoryName.contains(query) || + vendorDisplayName.contains(query); + }).toList(); + + if (services.isEmpty) { + return Center( + child: Padding( + padding: EdgeInsets.only( + top: ResponsiveSize.getHeightPercentage( + context, + 0.1, + ), + ), + child: Text( + _searchQuery.isNotEmpty + ? 'No services found for "$_searchQuery"' + : 'No services available.', + style: TextStyle( + fontSize: ResponsiveSize.getFontSize(context, 16), + ), + ), + ), + ); + } + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: services.length, + itemBuilder: (context, index) { + final service = services[index]; + + return Padding( + padding: EdgeInsets.only( + bottom: ResponsiveSize.getHeightPercentage( + context, + 0.02, + ), + ), + child: GestureDetector( + onTap: () { + Get.toNamed( + RouterConts.detailserivce, + arguments: service.id, + ); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage( + context, + 0.05, + ), + ), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage( + context, + 0.03, + ), + ), + child: + service.images1 != null && + service.images1!.isNotEmpty + ? CachedNetworkImage( + imageUrl: + service.images1!.first, + height: + ResponsiveSize.getHeightPercentage( + context, + 0.18, + ), + width: double.infinity, + fit: BoxFit.cover, + placeholder: (context, url) => + Container( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.18, + ), + width: double.infinity, + color: Colors.grey[300], + child: Icon( + Icons.image, + size: 50, + color: Colors.grey, + ), + ), + errorWidget: + ( + context, + url, + error, + ) => Container( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.18, + ), + width: double.infinity, + color: Colors.grey[300], + child: Icon( + Icons.broken_image, + size: + ResponsiveSize.getWidthPercentage( + context, + 0.1, + ), + color: Colors.grey, + ), + ), + ) + : Container( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.18, + ), + width: double.infinity, + color: Colors.grey[300], + child: Icon( + Icons.image, + size: 50, + color: Colors.grey, + ), + ), + ), + + Positioned( + top: ResponsiveSize.getHeightPercentage( + context, + 0.008, + ), + left: ResponsiveSize.getWidthPercentage( + context, + 0.015, + ), + child: Container( + width: + ResponsiveSize.getWidthPercentage( + context, + 0.2, + ), + height: + ResponsiveSize.getHeightPercentage( + context, + 0.035, + ), + padding: EdgeInsets.symmetric( + horizontal: + ResponsiveSize.getWidthPercentage( + context, + 0.015, + ), + vertical: + ResponsiveSize.getHeightPercentage( + context, + 0.002, + ), + ), + decoration: BoxDecoration( + color: service.serviceType == 1 + ? Color(0xFFFFE9E9) + : Color(0xFFE9FFEF), + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage( + context, + 0.04, + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + service.serviceType == 1 + ? AppAssets.paidicon + : AppAssets.freeicon, + width: + ResponsiveSize.getWidthPercentage( + context, + 0.045, + ), + height: + ResponsiveSize.getWidthPercentage( + context, + 0.045, + ), + ), + SizedBox( + width: + ResponsiveSize.getWidthPercentage( + context, + 0.01, + ), + ), + Text( + service.serviceType == 1 + ? 'Paid' + : 'Free', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: + ResponsiveSize.getFontSize( + context, + 14, + ), + height: 0.93, + letterSpacing: 1, + color: + service.serviceType == 1 + ? Colors.red + : Color(0xff3A942C), + ), + ), + ], + ), + ), + ), + ], + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage( + context, + 0.015, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: + ResponsiveSize.getWidthPercentage( + context, + 0.03, + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + service.vendorName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: + ResponsiveSize.getFontSize( + context, + 18, + ), + height: 1.1, + letterSpacing: 1, + ), + ), + ), + Row( + children: [ + Icon( + Icons.star, + color: Colors.orange, + size: + ResponsiveSize.getWidthPercentage( + context, + 0.04, + ), + ), + SizedBox( + width: + ResponsiveSize.getWidthPercentage( + context, + 0.01, + ), + ), + Text( + '4.5', + style: TextStyle( + fontSize: + ResponsiveSize.getFontSize( + context, + 14, + ), + ), + ), + ], + ), + ], + ), + SizedBox( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.005, + ), + ), + Text( + service.serviceName, + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: + ResponsiveSize.getFontSize( + context, + 14, + ), + height: 1.1, + letterSpacing: 1, + color: Color(0xFF5A5A5A), + ), + ), + SizedBox( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + + // Use responsive layout for the bottom row + LayoutBuilder( + builder: (context, constraints) { + // Determine layout based on available width instead of fixed breakpoints + bool useVerticalLayout = + constraints.maxWidth < 300; + + if (useVerticalLayout) { + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _buildVendorInfoContainer( + context, + service, + ), + SizedBox( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + _buildPriceContainer( + context, + service, + ), + ], + ); + } else { + // For normal screens, keep horizontal layout + return Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + _buildVendorInfoContainer( + context, + service, + ), + _buildPriceContainer( + context, + service, + ), + ], + ); + } + }, + ), + ], + ), + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage( + context, + 0.012, + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + loading: () => ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, // Show 3 skeleton items + itemBuilder: (context, index) { + return Padding( + padding: EdgeInsets.only( + bottom: ResponsiveSize.getHeightPercentage( + context, + 0.02, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage(context, 0.05), + ), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + children: [ + Container( + height: ResponsiveSize.getHeightPercentage( + context, + 0.18, + ), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage( + context, + 0.03, + ), + ), + ), + ), + Padding( + padding: EdgeInsets.all( + ResponsiveSize.getWidthPercentage( + context, + 0.03, + ), + ), + child: Column( + children: [ + Container( + height: 20, + color: Colors.grey.shade300, + ), + SizedBox(height: 8), + Container( + height: 16, + color: Colors.grey.shade300, + ), + SizedBox(height: 8), + Container( + height: 16, + color: Colors.grey.shade300, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + error: (error, _) => Center( + child: Padding( + padding: EdgeInsets.only( + top: ResponsiveSize.getHeightPercentage(context, 0.1), + ), + child: Text('Error loading services: $error'), + ), + ), + ), + // Add padding at the bottom for better scrolling experience + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.03), + ), + ], + ), + ), + ), + ), + ); + } + + // New method for View More bottom sheet + void showViewMoreBottomSheet( + BuildContext context, + AsyncValue> subcategoryAsyncValue, + ) { + // Reset temp selection to current selection + viewMoreSelectedIndex = selectedIndex; + tempSelectedSubcategoryId = selectedSubcategoryId; + final size = MediaQuery.of(context).size; + final isPortrait = + MediaQuery.of(context).orientation == Orientation.portrait; + final itemSize = isPortrait ? size.width * 0.16 : size.width * 0.1; + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (BuildContext context) { + return Consumer( + builder: (context, ref, child) { + // Fetch subcategories with category ID 0 for view more + final viewMoreSubcategoryAsyncValue = ref.watch( + subcategoryProvider( + "0", + ), // Always use category ID 0 for view more + ); + + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + height: MediaQuery.of(context).size.height * 0.8, + padding: EdgeInsets.all(20), + child: Column( + children: [ + // Header + Stack( + alignment: Alignment.center, + children: [ + // Back button (left-aligned) + Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.arrow_back_ios, size: 18), + ), + ), + // Centered title + Text( + 'Sub-categories', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: + MediaQuery.of(context).size.width * 0.045, + ), + ), + // Check button (right-aligned) + Align( + alignment: Alignment.centerRight, + child: FloatingActionButton( + mini: true, + backgroundColor: Colors.green, + onPressed: () { + setState(() { + selectedIndex = viewMoreSelectedIndex; + selectedSubcategoryId = + tempSelectedSubcategoryId; + selectedCategory = + tempSelectedSubcategoryId != null + ? "0" + : widget.id.toString(); + }); + Navigator.pop(context); + }, + child: Icon(Icons.check, color: Colors.white), + ), + ), + ], + ), + SizedBox(height: 20), + + // Content based on API data with category ID 0 + Expanded( + child: viewMoreSubcategoryAsyncValue.when( + loading: () => GridView.builder( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 1, + ), + itemCount: 6, // Show 6 skeleton items + itemBuilder: (context, index) { + return Column( + children: [ + Container( + height: itemSize, + width: itemSize, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + SizedBox(height: 8), + Container( + height: 12, + width: itemSize * 0.8, + color: Colors.grey.shade300, + ), + ], + ); + }, + ), + error: (err, stack) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, size: 48, color: Colors.red), + SizedBox(height: 16), + Text( + 'Error loading subcategories', + style: TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + '$err', + style: TextStyle(color: Colors.grey), + textAlign: TextAlign.center, + ), + SizedBox(height: 16), + ElevatedButton( + onPressed: () { + // Refresh the provider + ref.refresh(subcategoryProvider("0")); + }, + child: Text('Retry'), + ), + ], + ), + ), + data: (subcategories) { + print( + 'View More Subcategories loaded: ${subcategories.length}', + ); + + if (subcategories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.category, + size: 48, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'No subcategories found', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ); + } + + // Calculate total items (subcategories + 1 for "All") + int totalItems = subcategories.length + 1; + + return GridView.builder( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: + 1, // Changed to 1 for square items + ), + itemCount: totalItems, + itemBuilder: (context, index) { + bool isSelected = + index == viewMoreSelectedIndex; + + // First item is "All" + if (index == 0) { + return GestureDetector( + onTap: () { + setModalState(() { + viewMoreSelectedIndex = 0; + tempSelectedSubcategoryId = null; + }); + }, + child: Column( + children: [ + Container( + height: itemSize, + width: itemSize, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade100 + : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Image.asset(AppAssets.allicon), + ), + SizedBox( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + Text( + 'All', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: + ResponsiveSize.getFontSize( + context, + 12, + ), + fontWeight: FontWeight.w500, + color: isSelected + ? Colors.black + : Colors.grey, + ), + ), + ], + ), + ); + } + + // Subcategory items + final subcategory = subcategories[index - 1]; + return GestureDetector( + onTap: () { + setModalState(() { + viewMoreSelectedIndex = index; + tempSelectedSubcategoryId = subcategory.id + .toString(); + }); + }, + child: Column( + children: [ + Container( + height: itemSize, + width: itemSize, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade100 + : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Padding( + padding: EdgeInsets.all( + itemSize * 0.18, + ), + child: CachedNetworkImage( + imageUrl: subcategory.icon1, + fit: BoxFit.contain, + placeholder: (context, url) => Icon( + Icons.category, + size: 20, + color: Colors.grey, + ), + errorWidget: + (context, url, error) => Icon( + Icons.broken_image, + color: Colors.grey, + ), + ), + ), + ), + SizedBox( + height: + ResponsiveSize.getHeightPercentage( + context, + 0.01, + ), + ), + SizedBox( + width: itemSize, + child: Text( + subcategory.name, + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + maxLines: 2, + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: + ResponsiveSize.getFontSize( + context, + 12, + ), + height: 1.2, + color: isSelected + ? Colors.black + : Color(0xff909090), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ); + }, + ); + } + + Widget _buildVendorInfoContainer(BuildContext context, dynamic service) { + final size = MediaQuery.of(context).size; + final useSmallLayout = size.width < 320; + + return Container( + padding: EdgeInsets.all(ResponsiveSize.getWidthPercentage(context, 0.02)), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE9E9E9), width: 0.96), + color: const Color(0xFFF3F3F3), + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage(context, 0.06), + ), + ), + width: useSmallLayout + ? ResponsiveSize.getWidthPercentage(context, 0.85) + : ResponsiveSize.getWidthPercentage(context, 0.5), + + child: Row( + children: [ + CircleAvatar( + radius: ResponsiveSize.getWidthPercentage(context, 0.05), + backgroundImage: AssetImage(AppAssets.login), + ), + SizedBox(width: ResponsiveSize.getWidthPercentage(context, 0.02)), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + service.vendorDisplayName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: ResponsiveSize.getFontSize(context, 14), + height: 1.1, + color: Color(0xFF353434), + ), + ), + SizedBox( + height: ResponsiveSize.getHeightPercentage(context, 0.005), + ), + Text( + service.categoryName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: ResponsiveSize.getFontSize(context, 12), + height: 1.1, + color: Color(0xFF717171), + ), + ), + ], + ), + ), + ], + ), + ); + } + + // Price container extracted as a method for reuse + Widget _buildPriceContainer(BuildContext context, dynamic service) { + final size = MediaQuery.of(context).size; + final useSmallLayout = size.width < 320; + + return Container( + height: ResponsiveSize.getHeightPercentage(context, 0.035), + width: useSmallLayout + ? ResponsiveSize.getWidthPercentage(context, 0.5) + : ResponsiveSize.getWidthPercentage(context, 0.25), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular( + ResponsiveSize.getWidthPercentage(context, 0.05), + ), + border: Border.all(color: Colors.white, width: 1.5), + ), + child: Center( + child: Text( + 'Rs.${service.amount}', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: ResponsiveSize.getFontSize(context, 14), + ), + ), + ), + ); + } + + // Fix for the type mismatch issue in showCommanFilterBottomSheet method + void showCommanFilterBottomSheet(BuildContext context) { + // Local variables to track selection within the bottom sheet + String? localSelectedCategory; + String? localSelectedSubcategory; + String? localSelectedType; + + // Local subcategories list for the bottom sheet + List localSubcategories = List.from(subcategories); + + // Local categories list with a check to ensure it's not empty + List localCategories = List.from(categories); + + // If categories are empty, load them directly before showing the sheet + if (localCategories.isEmpty) { + final categoryRepo = ref.read(categoryRepositoryProvider); + categoryRepo + .fetchCategories(ConstsApi.catgories) + .then((loadedCategories) { + localCategories = loadedCategories; + // Continue showing the bottom sheet after categories are loaded + _displayBottomSheet( + context, + localCategories, + localSubcategories, + localSelectedCategory, + localSelectedSubcategory, + localSelectedType, + ); + }) + .catchError((e) { + print('Error loading categories for filter: $e'); + // Show the bottom sheet anyway, but with empty categories + _displayBottomSheet( + context, + [], + localSubcategories, + localSelectedCategory, + localSelectedSubcategory, + localSelectedType, + ); + }); + } else { + // Categories already loaded, show the bottom sheet directly + _displayBottomSheet( + context, + localCategories, + localSubcategories, + localSelectedCategory, + localSelectedSubcategory, + localSelectedType, + ); + } + } + + // Extracted the bottom sheet UI into a separate method + void _displayBottomSheet( + BuildContext context, + List localCategories, + List localSubcategories, + String? localSelectedCategory, + String? localSelectedSubcategory, + String? localSelectedType, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + // Helper function to load subcategories within the modal + Future loadSubcategoriesForModal(String categoryId) async { + final subcategoryRepo = ref.read(subcategoryRepositoryProvider); + try { + final newSubcategories = await subcategoryRepo + .fetchSubcategories(ConstsApi.subcat, categoryId); + + // Update the local subcategories using setModalState + setModalState(() { + localSubcategories = newSubcategories; + }); + } catch (e) { + print('Error loading subcategories: $e'); + setModalState(() { + localSubcategories = []; + }); + } + } + + return Container( + height: 809, + margin: const EdgeInsets.only(top: 210), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(40), + topRight: Radius.circular(40), + ), + border: Border.all(color: const Color(0xFF858181), width: 1), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 45.0, + vertical: 35, + ), + child: Column( + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Filter", + style: TextStyle( + fontFamily: 'Gilroy', + fontWeight: FontWeight.w900, + fontSize: 25, + height: 1.0, + ), + ), + IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: Image.asset( + AppAssets.filtericon, + height: 25, + width: 25, + color: const Color(0xff797777), + ), + ), + ], + ), + + const SizedBox(height: 40), + + // Category Dropdown with localized category data + Container( + width: 361, + height: 58, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFFDFDFD), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFD3D3D3), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + hint: Text("Select Category"), + value: localCategories.isEmpty + ? null + : (localCategories.any( + (cat) => + cat.id.toString() == + localSelectedCategory, + ) + ? localSelectedCategory + : null), + isExpanded: true, + items: localCategories.isEmpty + ? [ + DropdownMenuItem( + value: "no_categories", + child: Text("No categories available"), + ), + ] + : localCategories.map((category) { + return DropdownMenuItem( + value: category.id.toString(), + child: Text(category.name), + ); + }).toList(), + onChanged: localCategories.isEmpty + ? null + : (value) { + if (value != null && + value != "no_categories") { + // First reset the subcategory and clear local subcategories + setModalState(() { + localSelectedCategory = value; + localSelectedSubcategory = + null; // Reset subcategory selection + localSubcategories = + []; // Clear subcategories while loading + }); + + // Then load subcategories for selected category + loadSubcategoriesForModal(value); + } + }, + ), + ), + ), + + // Rest of your bottom sheet code remains the same... + const SizedBox(height: 20), + + // Subcategory Dropdown + Container( + width: 361, + height: 58, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFFDFDFD), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFD3D3D3), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + hint: Text("Select Subcategory"), + value: localSubcategories.isEmpty + ? null + : (localSubcategories.any( + (subcat) => + subcat.id.toString() == + localSelectedSubcategory, + ) + ? localSelectedSubcategory + : null), + isExpanded: true, + items: localSubcategories.isEmpty + ? [ + DropdownMenuItem( + value: "no_subcategories", + child: Text("No subcategories available"), + ), + ] + : localSubcategories.map((subcategory) { + return DropdownMenuItem( + value: subcategory.id.toString(), + child: Text(subcategory.name), + ); + }).toList(), + onChanged: localSubcategories.isEmpty + ? null + : (value) { + if (value != null && + value != "no_subcategories") { + setModalState(() { + localSelectedSubcategory = value; + }); + } + }, + ), + ), + ), + + const SizedBox(height: 20), + + // Type Dropdown + Container( + width: 361, + height: 58, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFFDFDFD), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFD3D3D3), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + hint: Text("Select Type"), + value: + (localSelectedType == "Free" || + localSelectedType == "Paid") + ? localSelectedType + : null, + isExpanded: true, + items: ["Free", "Paid"].map((type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), + onChanged: (value) { + setModalState(() { + localSelectedType = value; + }); + }, + ), + ), + ), + SizedBox(height: 50), + // Save Button + SizedBox( + width: double.infinity, + height: 58, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF000000), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: Color(0xFFD3D3D3), + width: 1, + ), + ), + ), + // Save Button + onPressed: () { + // Apply filters and update parent state + setState(() { + // Store the selected category from filter (only if valid selection) + if (localSelectedCategory != null && + localSelectedCategory != "no_categories") { + selectedCategory = localSelectedCategory; + } + if (localSelectedSubcategory != null && + localSelectedSubcategory != + "no_subcategories") { + selectedSubcategory = localSelectedSubcategory; + } + if (localSelectedType != null) { + selectedType = localSelectedType; + } + + // Update the class level categories and subcategories + categories = List.from(localCategories); + subcategories = List.from(localSubcategories); + + // Special case: if no category is selected, use the widget's default category + selectedCategory ??= widget.id.toString(); + + // Reset subcategory index if we're returning to the main category + if (selectedCategory == widget.id.toString()) { + selectedIndex = 0; // Set to "All" + } + + // Update subcategory ID for service provider + if (selectedSubcategory != null && + selectedSubcategory != "no_subcategories") { + selectedSubcategoryId = selectedSubcategory; + } else { + selectedSubcategoryId = + null; // Reset if none selected + } + + // Convert type string to number for service provider + if (localSelectedType != null) { + if (localSelectedType == "Paid") { + selectedTypeValue = '1'; + } else if (localSelectedType == "Free") { + selectedTypeValue = 'free'; + } + } else { + selectedTypeValue = + null; // Reset if none selected + } + + print( + "Filter applied - Category: ${selectedCategory ?? widget.id.toString()}, Subcategory: $selectedSubcategoryId, Type: $selectedTypeValue", + ); + }); + + Navigator.pop(context); + }, + + child: const Text( + 'Save', + style: TextStyle( + color: Colors.white, + fontFamily: 'Gilroy', + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/view/user_main_screens/main_contoller.dart b/lib/view/user_main_screens/main_contoller.dart new file mode 100644 index 0000000..3879925 --- /dev/null +++ b/lib/view/user_main_screens/main_contoller.dart @@ -0,0 +1,377 @@ +import 'dart:convert'; +import 'dart:ui'; +import 'dart:math' as math; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; // Add this import +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; + +class MainController extends ConsumerStatefulWidget { + final Widget child; + final int? initialBottomIndex; + + const MainController({ + super.key, + required this.child, + this.initialBottomIndex, + }); + + @override + ConsumerState createState() => _MainControllerState(); +} + +class _MainControllerState extends ConsumerState + with SingleTickerProviderStateMixin { + int _selectedIndex = 0; + bool _showMenu = false; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + + // Set bottom navigation index + if (widget.initialBottomIndex != null) { + _selectedIndex = widget.initialBottomIndex!; + } else { + // Auto-detect based on current route + final currentRoute = Get.currentRoute; + if (currentRoute == RouterConts.homescreen) { + _selectedIndex = 0; + } else if (currentRoute == RouterConts.packageList) { + _selectedIndex = 1; + } else if (currentRoute == RouterConts.categorypage) { + _selectedIndex = 2; + } else if (currentRoute == RouterConts.history) { + _selectedIndex = 3; + } else if (currentRoute == RouterConts.profilemainscreen || + currentRoute == RouterConts.editprofile || + currentRoute == RouterConts.changepassword) { + _selectedIndex = 0; // No tab selected + } + } + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + } + + void _toggleMenu() { + setState(() { + _showMenu = !_showMenu; + _showMenu + ? _animationController.forward() + : _animationController.reverse(); + }); + } + + // 🎯 NEW: Handle back button press + Future _onWillPop() async { + final currentRoute = Get.currentRoute; + + // If menu is open, close it first + if (_showMenu) { + _toggleMenu(); + return false; // Don't exit + } + + // Handle back navigation based on current route + switch (currentRoute) { + case RouterConts.profilemainscreen: + case RouterConts.editprofile: + case RouterConts.changepassword: + // Navigate back to home when coming from profile screens + Get.offAllNamed(RouterConts.homescreen); + return false; // Don't use default back behavior + + case RouterConts.packageList: + case RouterConts.categorypage: + case RouterConts.history: + // For other main tabs, go to home first + Get.offAllNamed(RouterConts.homescreen); + return false; + + case RouterConts.homescreen: + // If already on home, allow default back behavior (exit app) + return true; + + default: + // For any other screens, try to go back to home + try { + Get.back(); + } catch (e) { + // If can't go back, navigate to home + Get.offAllNamed(RouterConts.homescreen); + } + return false; + } + } + + void _onItemTapped(int index, BuildContext context) { + if (_selectedIndex == index) return; + + switch (index) { + case 0: + Get.offAllNamed(RouterConts.homescreen); + break; + case 1: + Get.offAllNamed(RouterConts.packageList); + break; + case 2: + Get.offAllNamed(RouterConts.categorypage); + break; + case 3: + // Navigate to history with default tab (Service Booking) + Get.offAllNamed( + RouterConts.history, + arguments: { + 'historyTab': 0, // Default to first tab + }, + ); + break; + } + } + + void changeSelectedIndex(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + final isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0; + + // 🎯 Wrap with WillPopScope to handle back button + return WillPopScope( + onWillPop: _onWillPop, + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + InheritedIndexController( + changeIndex: changeSelectedIndex, + child: widget.child, + ), + if (_showMenu && !isKeyboardOpen) + AnimatedOpacity( + opacity: 1.0, + duration: const Duration(milliseconds: 300), + child: GestureDetector( + onTap: _toggleMenu, + child: Container( + alignment: Alignment.center, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), + child: Container( + color: Colors.black.withOpacity(0.2), + width: double.infinity, + height: double.infinity, + ), + ), + ), + ), + ), + if (_showMenu && !isKeyboardOpen) + Positioned( + bottom: 60, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 110), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildMenuOption( + icon: Icons.store, + label: 'Vendor', + color: const Color(0xFF0066FF), + onTap: () async { + _toggleMenu(); + try { + await ref.read(getvendorIdProvider.future); + Get.toNamed(RouterConts.vendorwelcome); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to fetch Vendor ID'), + ), + ); + } + }, + ), + _buildMenuOption( + icon: Icons.person, + label: 'Profile', + color: const Color(0xFF0066FF), + onTap: () { + _toggleMenu(); + Get.toNamed(RouterConts.profilemainscreen); + }, + ), + ], + ), + ), + ), + ], + ), + floatingActionButton: isKeyboardOpen + ? null + : FloatingActionButton( + heroTag: "main_controller_fab", + onPressed: _toggleMenu, + shape: const CircleBorder(), + backgroundColor: const Color(0xFF0066FF), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.rotate( + angle: _animationController.value * 0.75 * math.pi, + child: Icon( + _animationController.value > 0.5 + ? Icons.close + : Icons.add, + color: Colors.white, + ), + ); + }, + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: BottomAppBar( + notchMargin: 8, + color: Colors.white, + elevation: 8, + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _bottomNavItem(0, AppAssets.home), + _bottomNavItem(1, AppAssets.package), + const Expanded(child: SizedBox()), + _bottomNavItem(2, AppAssets.categories), + _bottomNavItem(3, AppAssets.history), + ], + ), + ), + ), + ), + ); + } + + Widget _bottomNavItem(int index, String assetPath) { + return Expanded( + child: InkWell( + onTap: () => _onItemTapped(index, context), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + assetPath, + color: _selectedIndex == index + ? const Color(0xFF0066FF) + : Colors.grey, + width: 26, + height: 29, + ), + const SizedBox(height: 4), + Container( + height: 3, + width: 30, + decoration: BoxDecoration( + color: _selectedIndex == index + ? const Color(0xFF0066FF) + : Colors.transparent, + borderRadius: BorderRadius.circular(2), + ), + ), + ], + ), + ), + ); + } + + Widget _buildMenuOption({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return ScaleTransition( + scale: CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 3, + spreadRadius: 1, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon(icon, color: Colors.white), + onPressed: onTap, + padding: EdgeInsets.zero, + ), + ), + const SizedBox(height: 5), + Text( + label, + style: const TextStyle( + color: Color(0xFF0066FF), + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} + +class InheritedIndexController extends InheritedWidget { + final void Function(int) changeIndex; + + const InheritedIndexController({ + super.key, + required this.changeIndex, + required super.child, + }); + + static InheritedIndexController? of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(InheritedIndexController oldWidget) { + return oldWidget.changeIndex != changeIndex; + } +} diff --git a/lib/view/user_main_screens/most_popluar_viewall.dart b/lib/view/user_main_screens/most_popluar_viewall.dart new file mode 100644 index 0000000..aba1179 --- /dev/null +++ b/lib/view/user_main_screens/most_popluar_viewall.dart @@ -0,0 +1,401 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:get/get.dart'; + +class MostPopluarViewall extends ConsumerStatefulWidget { + const MostPopluarViewall({super.key}); + + @override + ConsumerState createState() => _MostPopluarViewallState(); +} + +class _MostPopluarViewallState extends ConsumerState { + bool _isValidCompleteUrl(String? url) { + if (url == null || url.isEmpty) return false; + try { + final uri = Uri.parse(url); + return uri.isAbsolute && uri.path.isNotEmpty; + } catch (e) { + return false; + } + } + + @override + Widget build(BuildContext context) { + final popularServicesAsync = ref.watch(mostPopularProvider('0')); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: popularServicesAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + data: (services) { + if (services.isEmpty) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Center( + child: Text("No services available for this category"), + ), + ); + } + + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.centerLeft, + child: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () => Navigator.pop(context), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text( + "Most popular service", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: MediaQuery.of(context).size.width / 2, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 0.74, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final service = services[index]; + return LayoutBuilder( + builder: (context, constraints) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFFFCFAFA), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFFE3E3E3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Service image with CachedNetworkImage + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(19), + ), + child: SizedBox( + height: constraints.maxWidth * 0.6, + width: double.infinity, + child: + _isValidCompleteUrl( + service.images1?.first, + ) + ? CachedNetworkImage( + imageUrl: service.images1!.first, + width: double.infinity, + height: constraints.maxWidth * 0.6, + fit: BoxFit.cover, + placeholder: (context, url) => + Container( + color: Colors.grey[200], + child: const Center( + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ), + errorWidget: + (context, url, error) => + Image.asset( + AppAssets.cleaning, + width: double.infinity, + height: + constraints.maxWidth * + 0.6, + fit: BoxFit.cover, + ), + ) + : Image.asset( + AppAssets.cleaning, + width: double.infinity, + height: constraints.maxWidth * 0.6, + fit: BoxFit.cover, + ), + ), + ), + + // Rest of your widget code remains the same... + Align( + alignment: Alignment.centerRight, + child: Container( + height: 30, + width: 80, + transform: Matrix4.translationValues( + -10, + -15, + 0, + ), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white, + width: 1.5, + ), + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), + child: Text( + 'Rs.${service.amount ?? "N/A"}', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 16.86, + height: 1.0, + letterSpacing: 0.02, + color: Color(0xFFFCFAFA), + ), + ), + ), + ), + ), + ), + ), + + // Service details + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB( + 8, + 0, + 8, + 8, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + service.serviceName ?? + 'Service', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 15.02, + height: 1.0, + letterSpacing: 0.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + color: Colors.orange, + size: 18, + ), + const SizedBox(width: 2), + Text( + service.averageReview + ?.toString() ?? + '0.0', + style: const TextStyle( + fontFamily: 'SF UI Display', + fontWeight: FontWeight.w600, + fontSize: 14, + height: 1.0, + letterSpacing: -0.5, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all( + 4, + ), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.lightGrey, + ), + color: Colors.white, + borderRadius: + BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + children: [ + _isValidCompleteUrl( + service.profilePic1, + ) + ? CircleAvatar( + radius: 15, + backgroundImage: + CachedNetworkImageProvider( + service + .profilePic1!, + ), + ) + : const CircleAvatar( + radius: 15, + backgroundImage: + AssetImage( + AppAssets + .login, + ), + ), + const SizedBox(width: 4), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisSize: + MainAxisSize.min, + children: [ + Text( + service.vendorName ?? + 'Vendor', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight + .w400, + fontSize: 11.75, + height: 1.0, + letterSpacing: + 0.0, + color: Color( + 0xFF353434, + ), + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + const SizedBox( + height: 2, + ), + Text( + service.serviceName ?? + 'Service', + style: const TextStyle( + fontFamily: + 'Gilroy-Regular', + fontWeight: + FontWeight + .w400, + fontSize: 11.75, + height: 1.0, + letterSpacing: + 0.0, + color: Color( + 0xFF717171, + ), + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () { + Get.toNamed( + RouterConts.detailserivce, + arguments: service.id, + ); + }, + child: Container( + padding: const EdgeInsets.all( + 8, + ), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: + BorderRadius.circular(12), + ), + child: const Icon( + Icons + .arrow_forward_ios_rounded, + size: 16, + color: Colors.white, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + }, childCount: services.length), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/view/user_main_screens/notification_page.dart b/lib/view/user_main_screens/notification_page.dart new file mode 100644 index 0000000..dabd158 --- /dev/null +++ b/lib/view/user_main_screens/notification_page.dart @@ -0,0 +1,165 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/model/notification_model.dart'; +import 'package:bookmywages/view/auth/auth_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// Badge count state provider +final notificationCountProvider = StateProvider((ref) => 0); + +// Repository provider +final notificationRepositoryProvider = Provider( + (ref) => NotificationRepository(), +); + +// Future provider with parameters +final notificationProvider = FutureProvider.family + .autoDispose, ({int type, String userId})>(( + ref, + params, + ) async { + final repo = ref.read(notificationRepositoryProvider); + return repo.fetchNotification(type: params.type, userId: params.userId); + }); + +class NotificationPage extends ConsumerWidget { + final int type; + final String id; + + const NotificationPage({super.key, required this.type, required this.id}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncNotif = ref.watch( + notificationProvider((type: type, userId: id)), + ); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back_ios), + ), + ), + const Center( + child: Text( + "Notification", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + + // Notification list with badge update + asyncNotif.when( + loading: () => + const Center(child: CircularProgressIndicator()), + error: (err, _) => Center(child: Text('Error: $err')), + data: (notifications) { + Future.microtask(() { + ref.read(notificationCountProvider.notifier).state = + notifications.length; + }); + + if (notifications.isEmpty) { + return const Center(child: Text("No Notification")); + } + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: notifications.length, + itemBuilder: (context, index) { + final item = notifications[index]; + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 67.08, + height: 67.08, + margin: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(9.75), + border: Border.all( + color: const Color(0xFFB7B7B7), + width: 0.98, + ), + ), + child: _buildNotificationImage(item.images), + ), + Expanded( + child: Text( + item.message, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15, + height: 28 / 15, + letterSpacing: 0.15, + color: AppColors.thridprimary, + ), + ), + ), + ], + ); + }, + ); + }, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildNotificationImage(List images) { + if (images.isEmpty || images[0].isEmpty) { + return const Icon(Icons.notifications, size: 30, color: Colors.grey); + } + + return ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.network( + images[0], + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.broken_image, size: 30, color: Colors.grey); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + }, + ), + ); + } +} diff --git a/lib/view/user_main_screens/package_screen.dart b/lib/view/user_main_screens/package_screen.dart new file mode 100644 index 0000000..be60c71 --- /dev/null +++ b/lib/view/user_main_screens/package_screen.dart @@ -0,0 +1,370 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/model/package_model.dart'; +import 'package:bookmywages/model/plan_sucess_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class PackageScreen extends ConsumerStatefulWidget { + const PackageScreen({super.key}); + + @override + ConsumerState createState() => _PackageScreenState(); +} + +class _PackageScreenState extends ConsumerState { + bool _isMounted = false; + + @override + void initState() { + super.initState(); + _isMounted = true; + } + + @override + void dispose() { + _isMounted = false; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final packageAsyncValue = ref.watch(pacakgeProvider); + final indexController = InheritedIndexController.of(context); + + return PopScope( + onPopInvoked: (didPop) { + if (didPop) { + // Reset main controller index to 0 when going back + final indexController = InheritedIndexController.of(context); + indexController?.changeIndex(0); + } + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon( + Icons.arrow_back_ios_new_sharp, + color: Colors.black, + ), + onPressed: () => Navigator.pop(context), + ), + centerTitle: true, + title: const Text( + "Subscription Plan", + style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + ), + ), + extendBodyBehindAppBar: true, + body: LayoutBuilder( + builder: (context, constraints) { + final isSmallScreen = constraints.maxWidth < 360; + final cardWidth = constraints.maxWidth * 0.8; + final cardHeight = constraints.maxHeight * 0.4; + + return Container( + height: double.infinity, + width: double.infinity, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage(AppAssets.background), + fit: BoxFit.cover, + ), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: constraints.maxWidth * 0.04, + vertical: constraints.maxHeight * 0.02, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: constraints.maxHeight * 0.2, + ), + child: Image.asset( + AppAssets.subscriptionimage, + fit: BoxFit.contain, + ), + ), + SizedBox(height: constraints.maxHeight * 0.02), + Text( + "Choose your right plan", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: isSmallScreen ? 24 : 32, + height: 1.0, + letterSpacing: 0.32, + color: Colors.black, + ), + ), + SizedBox(height: constraints.maxHeight * 0.01), + Padding( + padding: EdgeInsets.symmetric( + horizontal: constraints.maxWidth * 0.05, + ), + child: Text( + "Make upgrade your plan and get more advantages of services", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: isSmallScreen ? 13 : 15, + height: 1.71, + letterSpacing: 0.15, + color: Colors.black, + ), + ), + ), + SizedBox(height: constraints.maxHeight * 0.02), + Expanded( + child: packageAsyncValue.when( + data: (packages) { + final filteredPackages = packages + .where((pkg) => pkg.type == 1) + .toList(); + + return Center( + child: SizedBox( + height: cardHeight, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: filteredPackages.length, + itemBuilder: (context, index) { + final PackageModel package = + filteredPackages[index]; + + return Container( + width: cardWidth, + margin: EdgeInsets.symmetric( + horizontal: constraints.maxWidth * 0.02, + ), + constraints: BoxConstraints( + maxHeight: cardHeight, + ), + decoration: BoxDecoration( + color: const Color(0xFF334E95), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: const Color(0xFF334E95), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x40828282), + offset: Offset(0, 5), + blurRadius: 4, + ), + ], + ), + child: _PackageCard( + package: package, + onSubscribe: () => _subscribeToPlan( + package, + indexController, + ), + isSmallScreen: isSmallScreen, + ), + ); + }, + ), + ), + ); + }, + loading: () => + const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Text( + 'Error loading packages: $error', + style: const TextStyle(color: Colors.red), + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } + + Future _subscribeToPlan( + PackageModel package, + InheritedIndexController? indexController, + ) async { + try { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final model = PlanSuccessModel( + userId: userId, + planId: package.id.toString(), + duration: package.duration, + type: package.type, + ); + + final result = await ref.read(planSuccessProvider(model).future); + + if (_isMounted) { + if (result) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Success'), + content: const Text('Plan subscribed successfully!'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + indexController?.changeIndex(0); + ref.invalidate(expiredPlanProvider); + Get.toNamed(RouterConts.homescreen); + }, + child: const Text('OK'), + ), + ], + ), + ); + } else { + Fluttertoast.showToast( + msg: 'Failed to subscribe to plan', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + } catch (error) { + if (_isMounted) { + Fluttertoast.showToast( + msg: 'Error: ${error.toString()}', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + } +} + +class _PackageCard extends StatelessWidget { + final PackageModel package; + final VoidCallback onSubscribe; + final bool isSmallScreen; + + const _PackageCard({ + required this.package, + required this.onSubscribe, + required this.isSmallScreen, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(MediaQuery.of(context).size.width * 0.04), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + package.name, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: isSmallScreen ? 16 : 18, + height: 1.0, + letterSpacing: 0.32, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + _buildFeatureRow(package.description), + const SizedBox(height: 16), + _buildFeatureRow(_getFormattedDuration(package.duration)), + const SizedBox(height: 16), + _buildFeatureRow('${package.noOfService} book service'), + ], + ), + const Divider(color: Colors.white), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + package.price, + style: TextStyle( + fontSize: isSmallScreen ? 14 : 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + GestureDetector( + onTap: onSubscribe, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_forward, color: Colors.black), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFeatureRow(String text) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar(radius: 8, backgroundColor: Color(0xFFF9E369)), + const SizedBox(width: 6), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: isSmallScreen ? 12 : 14, + color: Colors.white, + ), + ), + ), + ], + ); + } + + String _getFormattedDuration(int duration) { + if (duration < 30) { + return '$duration days'; + } else { + int months = duration ~/ 30; + return '$months month${months > 1 ? 's' : ''}'; + } + } +} diff --git a/lib/view/user_main_screens/profile_screens/edit_profile.dart b/lib/view/user_main_screens/profile_screens/edit_profile.dart new file mode 100644 index 0000000..e981aea --- /dev/null +++ b/lib/view/user_main_screens/profile_screens/edit_profile.dart @@ -0,0 +1,456 @@ +import 'dart:io'; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_picker/image_picker.dart'; + +class EditProfile extends StatefulWidget { + const EditProfile({super.key}); + + @override + State createState() => _EditProfileState(); +} + +class _EditProfileState extends State { + File? _image; + final ImagePicker _picker = ImagePicker(); + + final TextEditingController nameController = TextEditingController(); + final TextEditingController numberController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController addressController = TextEditingController(); + + bool _isProfileDataInitialized = false; + + Future _pickImage() async { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + _image = File(pickedFile.path); + }); + } + } + + // Helper method to validate and construct image URL + String? _getValidImageUrl(String? imageUrl) { + if (imageUrl == null || imageUrl.isEmpty) { + return null; + } + + // Check if URL is complete (has a filename) + if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) { + return null; + } + + // Ensure URL is properly formatted + if (!imageUrl.startsWith('http')) { + return null; + } + + return imageUrl; + } + + // Helper widget for safe network image loading + Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) { + final validUrl = _getValidImageUrl(imageUrl); + + if (validUrl == null) { + return Image.asset(AppAssets.profile, fit: fit); + } + + return Image.network( + validUrl, + fit: fit, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + print('Error loading image: $error'); + return Image.asset(AppAssets.profile, fit: fit); + }, + ); + } + + // Helper method for CircleAvatar image provider + ImageProvider _getAvatarImageProvider(String? imageUrl) { + if (_image != null) { + return FileImage(_image!); + } + + final validUrl = _getValidImageUrl(imageUrl); + if (validUrl != null) { + return NetworkImage(validUrl); + } + + return const AssetImage(AppAssets.profile); + } + + final TextStyle gilroyTextStyle = const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.45, + letterSpacing: 0.01, + ); + + @override + void dispose() { + nameController.dispose(); + numberController.dispose(); + emailController.dispose(); + addressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + double width = MediaQuery.of(context).size.width; + double height = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: false, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + Stack( + children: [ + SizedBox( + width: double.infinity, + height: height * 0.4, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch( + profilegetuserProvider, + ); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + + if (profile != null && + !_isProfileDataInitialized) { + WidgetsBinding.instance + .addPostFrameCallback((_) { + setState(() { + nameController.text = + profile.name ?? ''; + numberController.text = + profile.number ?? ''; + emailController.text = + profile.email ?? ''; + addressController.text = + profile.address ?? ''; + _isProfileDataInitialized = true; + }); + }); + } + + return _image == null + ? _buildNetworkImage( + profile?.profilePic1, + ) + : Image.file( + _image!, + fit: BoxFit.cover, + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, _) => Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Icon(Icons.error, size: 50), + Text('Error loading profile: $error'), + ], + ), + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + child: CustomPaint( + size: Size(width, height * 0.4), + painter: RightToLeftFullWidthTrianglePainter(), + ), + ), + Positioned( + bottom: height * 0.03, + left: width / 2 - 170, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch( + profilegetuserProvider, + ); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + return CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: + _getAvatarImageProvider( + profile?.profilePic1, + ), + onBackgroundImageError: + (error, stackTrace) { + print( + 'Avatar image error: $error', + ); + }, + ), + ); + }, + loading: () => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + child: CircularProgressIndicator(), + ), + ), + error: (error, _) => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: AssetImage( + AppAssets.menu, + ), + ), + ), + ); + }, + ), + ), + Positioned( + bottom: height * 0.08, + left: width / 2 - 20, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch( + profilegetuserProvider, + ); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + if (profile == null) { + return Text( + 'No Profile Data', + style: gilroyTextStyle, + ); + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + profile.name ?? 'No Name', + style: gilroyTextStyle, + ), + IconButton( + onPressed: _pickImage, + icon: const Icon(Icons.edit), + ), + ], + ), + Row( + children: [ + const Icon( + Icons.location_city, + size: 20, + ), + const SizedBox(width: 4), + Text( + profile.address ?? + 'No address available', + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ), + ], + ); + }, + loading: () => + const CircularProgressIndicator(), + error: (error, _) => Text( + 'Error: $error', + style: gilroyTextStyle, + ), + ); + }, + ), + ), + ], + ), + const SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: width * 0.05), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Name", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your name", + controller: nameController, + ), + const SizedBox(height: 30), + Text("Mobile Number", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your mobile number", + keyboardType: TextInputType.phone, + controller: numberController, + ), + const SizedBox(height: 30), + Text("Address", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your address", + controller: addressController, + ), + const SizedBox(height: 30), + Text("Email", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your email", + keyboardType: TextInputType.emailAddress, + controller: emailController, + ), + const SizedBox(height: 30), + Center( + child: Consumer( + builder: (context, ref, _) { + return CommanButton( + width: width * 0.6, + text: "Save", + textStyle: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 23, + height: 1.0, + letterSpacing: 0.01, + color: AppColors.secondprimary, + ), + onPressed: () async { + try { + final repo = ref.read( + profileupdateRepositoryProvider, + ); + await repo.updateProfile( + url: ConstsApi.upadateprofile, + name: nameController.text, + number: numberController.text, + email: emailController.text, + address: addressController.text, + imageFile: _image, + ); + + if (mounted) { + Fluttertoast.showToast( + msg: "Profile updated successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + + ref.refresh(profilegetuserProvider); + + Future.delayed( + const Duration(milliseconds: 1500), + () { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: "Error updating profile: $e", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + }, + ); + }, + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } +} + +class RightToLeftFullWidthTrianglePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white; + final path = Path(); + path.moveTo(size.width, 0); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/view/user_main_screens/profile_screens/profile_changepass.dart b/lib/view/user_main_screens/profile_screens/profile_changepass.dart new file mode 100644 index 0000000..94f1b52 --- /dev/null +++ b/lib/view/user_main_screens/profile_screens/profile_changepass.dart @@ -0,0 +1,421 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fluttertoast/fluttertoast.dart'; + +class ProfileChangepass extends ConsumerStatefulWidget { + const ProfileChangepass({super.key}); + + @override + ConsumerState createState() => _ProfileChangepassState(); +} + +class _ProfileChangepassState extends ConsumerState { + final TextEditingController _newPasswordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + final _formKey = GlobalKey(); + bool _obscureNewPassword = true; + bool _obscureConfirmPassword = true; + String? _newPasswordError; + String? _confirmPasswordError; + + @override + void dispose() { + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _submitForm() async { + FocusScope.of(context).unfocus(); + + // Reset errors + setState(() { + _newPasswordError = null; + _confirmPasswordError = null; + }); + + if (_formKey.currentState!.validate()) { + try { + final success = await ref.read( + changepasswordProvider(_newPasswordController.text).future, + ); + + if (success && mounted) { + Fluttertoast.showToast( + msg: "Password changed successfully", + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + Navigator.pop(context); + } else { + Fluttertoast.showToast( + msg: "Failed to change password", + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: "Error: ${e.toString()}", + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + } + } + } + + String? _validateNewPassword(String? value) { + if (value == null || value.isEmpty) { + setState(() { + _newPasswordError = "Please enter a password"; + }); + return ""; + } + if (value.length < 6) { + // Fixed: was 4, should be 6 as per error message + setState(() { + _newPasswordError = "Password must be at least 6 characters"; + }); + return ""; + } + setState(() { + _newPasswordError = null; + }); + return null; + } + + String? _validateConfirmPassword(String? value) { + if (value == null || value.isEmpty) { + setState(() { + _confirmPasswordError = "Please confirm your password"; + }); + return ""; + } + if (value != _newPasswordController.text) { + setState(() { + _confirmPasswordError = "Passwords don't match"; + }); + return ""; + } + setState(() { + _confirmPasswordError = null; + }); + return null; + } + + @override + Widget build(BuildContext context) { + final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; + final isKeyboardVisible = keyboardHeight > 0; + final screenHeight = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: false, // Keep false to prevent white space + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + height: screenHeight, // Fixed height + color: AppColors.secondprimary, + child: SafeArea( + child: Column( + children: [ + // Header with back button + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon( + Icons.arrow_back_ios_new, + size: 20, + color: Colors.black, + ), + ), + const Expanded( + child: Center( + child: Text( + 'Change Password', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 20), + ], + ), + ), + + // Scrollable content with proper height calculation + Expanded( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: isKeyboardVisible ? keyboardHeight + 16 : 16.0, + ), + child: Column( + children: [ + // Show smaller image when keyboard is visible, normal size when not + if (!isKeyboardVisible) ...[ + Image.asset( + AppAssets.changepass, + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.height * 0.3, + fit: BoxFit.contain, + ), + const SizedBox(height: 16), + ] else ...[ + // Smaller image when keyboard is open + Image.asset( + AppAssets.changepass, + width: MediaQuery.of(context).size.width * 0.4, + height: screenHeight * 0.12, // Even smaller height + fit: BoxFit.contain, + ), + const SizedBox(height: 8), + ], + + // Form to change password + Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Create Password", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 20, + fontWeight: FontWeight.w700, + color: Colors.black87, + height: 18.68 / 20, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 53, + decoration: BoxDecoration( + color: const Color(0xFFFDFFFF), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: const Color(0xFF8C8A8A), + ), + ), + child: TextFormField( + controller: _newPasswordController, + obscureText: _obscureNewPassword, + decoration: InputDecoration( + hintText: "Enter new password", + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + border: InputBorder.none, + hintStyle: const TextStyle( + fontFamily: 'Martel', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 29 / 14, + letterSpacing: -0.5, + color: Color(0xFF757576), + ), + suffixIcon: IconButton( + icon: Icon( + _obscureNewPassword + ? Icons.visibility_off + : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscureNewPassword = + !_obscureNewPassword; + }); + }, + ), + ), + validator: _validateNewPassword, + ), + ), + SizedBox( + height: _newPasswordError != null ? 28 : 0, + child: + _newPasswordError != null + ? Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: 16.0, + ), + child: Text( + _newPasswordError!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + : null, + ), + ], + ), + + const SizedBox(height: 20), + const Text( + "Confirm Password", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 20, + fontWeight: FontWeight.w700, + color: Colors.black87, + height: 18.68 / 20, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 53, + decoration: BoxDecoration( + color: const Color(0xFFFDFFFF), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: const Color(0xFF8C8A8A), + ), + ), + child: TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + hintText: "Confirm your password", + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + border: InputBorder.none, + hintStyle: const TextStyle( + fontFamily: 'Martel', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 29 / 14, + letterSpacing: -0.5, + color: Color(0xFF757576), + ), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility_off + : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = + !_obscureConfirmPassword; + }); + }, + ), + ), + validator: _validateConfirmPassword, + ), + ), + SizedBox( + height: + _confirmPasswordError != null ? 28 : 0, + child: + _confirmPasswordError != null + ? Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: 16.0, + ), + child: Text( + _confirmPasswordError!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + : null, + ), + ], + ), + + const SizedBox(height: 20), + Center( + child: SizedBox( + width: 299, + height: 61.71, + child: ElevatedButton( + onPressed: _submitForm, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066FF), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 34.77, + ), + ), + elevation: 0, + ), + child: const Text( + "Save", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 28.76, + height: 1.0, + letterSpacing: 0.01, + ), + ), + ), + ), + ), + // Add some bottom spacing + SizedBox(height: isKeyboardVisible ? 16 : 30), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/user_main_screens/profile_screens/profile_main_page.dart b/lib/view/user_main_screens/profile_screens/profile_main_page.dart new file mode 100644 index 0000000..a4dbe82 --- /dev/null +++ b/lib/view/user_main_screens/profile_screens/profile_main_page.dart @@ -0,0 +1,668 @@ +import 'dart:ui'; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ProfileMainPage extends StatefulWidget { + const ProfileMainPage({super.key}); + + @override + State createState() => _ProfileMainPageState(); +} + +class _ProfileMainPageState extends State { + File? _image; + final ImagePicker _picker = ImagePicker(); + bool _isProfileDataInitialized = false; + bool _isUploading = false; + + // Modified _pickImage method with automatic API upload using existing repository + Future _pickImage() async { + try { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + _image = File(pickedFile.path); + _isUploading = true; + }); + + // Automatically upload to API using existing repository + await _uploadImageToAPI(File(pickedFile.path)); + } + } catch (e) { + print('Error picking image: $e'); + _showErrorToast('Failed to pick image: $e'); + } + } + + // Upload image using existing ProfileupdateRepository + Future _uploadImageToAPI(File imageFile) async { + try { + // Get current profile data + final profileData = ProviderScope.containerOf( + context, + ).read(profilegetuserProvider); + + await profileData.when( + data: (profiles) async { + if (profiles.isNotEmpty) { + final profile = profiles[0]; + + // Get the repository + final repository = ProviderScope.containerOf( + context, + ).read(profileupdateRepositoryProvider); + + // Update profile with new image and existing data + await repository.updateProfile( + url: ConstsApi.upadateprofile, // Use your existing API constant + name: profile.name, + number: profile.number, + email: profile.email, + address: profile.address ?? '', + imageFile: imageFile, + ); + + // Refresh profile data to get updated image URL + ProviderScope.containerOf(context).refresh(profilegetuserProvider); + ProviderScope.containerOf( + context, + ).refresh(profileupdateuserProvider); + + _showSuccessToast('Profile picture updated successfully!'); + + // Clear local image since it's now uploaded + setState(() { + _image = null; + }); + } else { + _showErrorToast('No profile data available'); + setState(() { + _image = null; + }); + } + }, + loading: () async { + _showErrorToast('Profile data is still loading'); + setState(() { + _image = null; + }); + }, + error: (error, _) async { + _showErrorToast('Error getting profile data: $error'); + setState(() { + _image = null; + }); + }, + ); + } catch (e) { + print('Error uploading image: $e'); + _showErrorToast('Upload failed: $e'); + setState(() { + _image = null; + }); + } finally { + setState(() { + _isUploading = false; + }); + } + } + + // Helper methods for showing toasts + void _showSuccessToast(String message) { + Fluttertoast.showToast( + msg: message, + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + + void _showErrorToast(String message) { + Fluttertoast.showToast( + msg: message, + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + + // Helper method to validate and construct image URL + String? _getValidImageUrl(String? imageUrl) { + if (imageUrl == null || imageUrl.isEmpty) { + return null; + } + + // Check if URL is complete (has a filename) + if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) { + return null; + } + + // Ensure URL is properly formatted + if (!imageUrl.startsWith('http')) { + return null; + } + + return imageUrl; + } + + // Helper widget for safe network image loading + Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) { + final validUrl = _getValidImageUrl(imageUrl); + + if (validUrl == null) { + return Image.asset(AppAssets.profile, fit: fit); + } + + return Image.network( + validUrl, + fit: fit, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + print('Error loading image: $error'); + return Image.asset(AppAssets.profile, fit: fit); + }, + ); + } + + // Helper method for CircleAvatar image provider + ImageProvider _getAvatarImageProvider(String? imageUrl) { + if (_image != null) { + return FileImage(_image!); + } + + final validUrl = _getValidImageUrl(imageUrl); + if (validUrl != null) { + return NetworkImage(validUrl); + } + + return const AssetImage(AppAssets.profile); + } + + final TextStyle gilroyTextStyle = const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.45, + letterSpacing: 0.01, + ); + + @override + Widget build(BuildContext context) { + double width = MediaQuery.of(context).size.width; + double height = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: Column( + children: [ + Flexible( + flex: 1, + child: Stack( + children: [ + SizedBox( + width: double.infinity, + height: height * 0.4, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch(profilegetuserProvider); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + + if (profile != null && !_isProfileDataInitialized) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + profile.address ?? ''; + _isProfileDataInitialized = true; + }); + }); + } + + return _image == null + ? _buildNetworkImage(profile?.profilePic1) + : Image.file(_image!, fit: BoxFit.cover); + }, + loading: () => + const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, size: 50), + Text('Error loading profile: $error'), + ], + ), + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + child: CustomPaint( + size: Size(width, height * 0.4), + painter: RightToLeftFullWidthTrianglePainter(), + ), + ), + Positioned( + bottom: height * 0.03, + left: width / 2 - 170, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch(profilegetuserProvider); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + return Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: _getAvatarImageProvider( + profile?.profilePic1, + ), + onBackgroundImageError: + (error, stackTrace) { + print('Avatar image error: $error'); + }, + ), + ), + // Show upload progress indicator + if (_isUploading) + Container( + width: 136, + height: 136, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ), + ), + ], + ); + }, + loading: () => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + child: CircularProgressIndicator(), + ), + ), + error: (error, _) => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: AssetImage(AppAssets.menu), + ), + ), + ); + }, + ), + ), + Positioned( + bottom: height * 0.08, + left: width / 2 - 20, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch(profilegetuserProvider); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + if (profile == null) { + return Text( + 'No Profile Data', + style: gilroyTextStyle, + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + profile.name ?? 'No Name', + style: gilroyTextStyle, + ), + IconButton( + onPressed: _isUploading + ? null + : _pickImage, + icon: _isUploading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.edit), + ), + ], + ), + Row( + children: [ + const Icon(Icons.location_city, size: 20), + const SizedBox(width: 4), + Text( + profile.address ?? 'No address available', + style: const TextStyle(fontSize: 12), + ), + ], + ), + ], + ); + }, + loading: () => const CircularProgressIndicator(), + error: (error, _) => + Text('Error: $error', style: gilroyTextStyle), + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 30), + Expanded( + flex: 1, + child: ListView( + shrinkWrap: false, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildMenuItem(Icons.person, 'My Profile'), + _buildMenuItem(Icons.lock, 'Change Password'), + _buildMenuItem(Icons.notifications, 'Notification'), + _buildMenuItem(Icons.description, 'Terms and Conditions'), + _buildMenuItem(Icons.logout, 'Sign out'), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildMenuItem(IconData icon, String title) { + return ListTile( + leading: Icon( + icon, + size: 23.000513076782227, + color: const Color(0xFF3E3E3F), + ), + title: Text( + title, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 18, + height: 1.0, + letterSpacing: 0.2, + color: Color(0xFF3E3E3F), + ), + ), + trailing: const Icon(Icons.arrow_forward_ios_rounded), + onTap: () async { + try { + if (title == 'My Profile') { + await Get.toNamed(RouterConts.editprofile); + } else if (title == 'Change Password') { + await Get.toNamed(RouterConts.changepassword); + } else if (title == 'Sign out') { + _showSignOutDialog(); + } else if (title == 'Terms and Conditions') { + _showTermsAndConditionsDialog(); // Removed ref parameter + } + } catch (e) { + print('Navigation error: $e'); + if (mounted) { + Fluttertoast.showToast( + msg: 'Navigation error: $e', + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + } + }, + ); + } + + void _showSignOutDialog() { + if (!mounted) return; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Sign Out'), + content: const Text('Are you sure you want to sign out?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('userId'); + await prefs.remove('vendor_id'); + await prefs.remove('data'); + + if (context.mounted) { + // context.go( + // RouterConts.loginpage, + // ); // or use pushReplacement if needed + } + }, + child: const Text('Sign Out'), + ), + ], + ); + }, + ); + } + + void _showTermsAndConditionsDialog() { + // Removed WidgetRef ref parameter + if (!mounted) return; + + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Center( + child: Consumer( + builder: (context, ref, _) { + final termsAsync = ref.watch(termsAndConditionsProvider); + + return termsAsync.when( + data: (terms) => Container( + width: 389, + height: 547, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFF959595), + width: 1, + ), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Text( + "Terms & Condition", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + const SizedBox(height: 12), + Expanded( + child: ScrollbarTheme( + data: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all( + AppColors.primary, + ), + trackColor: WidgetStateProperty.all( + Colors.transparent, + ), + trackBorderColor: WidgetStateProperty.all( + Colors.transparent, + ), + thickness: WidgetStateProperty.all(13), + radius: const Radius.circular(19), + thumbVisibility: WidgetStateProperty.all(true), + ), + child: Scrollbar( + thumbVisibility: true, + interactive: true, + thickness: 13, + radius: const Radius.circular(19), + child: ListView( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + terms.data, + style: const TextStyle( + color: Colors.black, + ), + ), + const SizedBox(height: 20), + Center( + child: SizedBox( + width: 228, + height: 51.82, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF0066FF, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 22.95, + ), + ), + ), + child: const Text( + "OK", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + fontSize: 18.87, + height: 23.59 / 18.87, + letterSpacing: -0.59, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ), + loading: () => + const Center(child: CircularProgressIndicator()), + error: (e, _) => Container( + padding: const EdgeInsets.all(16), + child: Text( + "Error: ${e.toString()}", + style: const TextStyle(color: Colors.red), + ), + ), + ); + }, + ), + ), + ), + ); + }, + ); + } +} + +class RightToLeftFullWidthTrianglePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white; + + final path = Path(); + path.moveTo(size.width, 0); // top right corner + path.lineTo(size.width, size.height); // bottom right corner + path.lineTo(0, size.height); // bottom left (tip) + path.close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/view/user_main_screens/sucessfull_screen.dart b/lib/view/user_main_screens/sucessfull_screen.dart new file mode 100644 index 0000000..db10ae0 --- /dev/null +++ b/lib/view/user_main_screens/sucessfull_screen.dart @@ -0,0 +1,51 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:flutter/material.dart'; + +class SucessfullScreen extends StatefulWidget { + const SucessfullScreen({super.key}); + + @override + State createState() => _SucessfullScreenState(); +} + +class _SucessfullScreenState extends State { + @override + void initState() { + super.initState(); + // Wait 3 seconds then go back + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + Navigator.pop(context); // This goes back to the previous screen + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset(AppAssets.sucess, width: 300, height: 300), + Text( + "Successful your Booking", + style: TextStyle( + fontFamily: 'Mogra', + fontWeight: FontWeight.w600, + fontSize: 22, + height: 57.82 / 25.46, // line-height ratio + letterSpacing: 1.0, + color: Color(0xFF0C2C5C), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/vendor_main_screens/Vendor_profile/vendor_edit_profile.dart b/lib/view/vendor_main_screens/Vendor_profile/vendor_edit_profile.dart new file mode 100644 index 0000000..724594f --- /dev/null +++ b/lib/view/vendor_main_screens/Vendor_profile/vendor_edit_profile.dart @@ -0,0 +1,456 @@ +import 'dart:io'; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_picker/image_picker.dart'; + +class VendorEditProfile extends StatefulWidget { + const VendorEditProfile({super.key}); + + @override + State createState() => _VendorEditProfileState(); +} + +class _VendorEditProfileState extends State { + File? _image; + final ImagePicker _picker = ImagePicker(); + + final TextEditingController nameController = TextEditingController(); + final TextEditingController numberController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController addressController = TextEditingController(); + + bool _isProfileDataInitialized = false; + + Future _pickImage() async { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + _image = File(pickedFile.path); + }); + } + } + + // Helper method to validate and construct image URL + String? _getValidImageUrl(String? imageUrl) { + if (imageUrl == null || imageUrl.isEmpty) { + return null; + } + + // Check if URL is complete (has a filename) + if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) { + return null; + } + + // Ensure URL is properly formatted + if (!imageUrl.startsWith('http')) { + return null; + } + + return imageUrl; + } + + // Helper widget for safe network image loading + Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) { + final validUrl = _getValidImageUrl(imageUrl); + + if (validUrl == null) { + return Image.asset(AppAssets.profile, fit: fit); + } + + return Image.network( + validUrl, + fit: fit, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + print('Error loading image: $error'); + return Image.asset(AppAssets.profile, fit: fit); + }, + ); + } + + // Helper method for CircleAvatar image provider + ImageProvider _getAvatarImageProvider(String? imageUrl) { + if (_image != null) { + return FileImage(_image!); + } + + final validUrl = _getValidImageUrl(imageUrl); + if (validUrl != null) { + return NetworkImage(validUrl); + } + + return const AssetImage(AppAssets.profile); + } + + final TextStyle gilroyTextStyle = const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.45, + letterSpacing: 0.01, + ); + + @override + void dispose() { + nameController.dispose(); + numberController.dispose(); + emailController.dispose(); + addressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + double width = MediaQuery.of(context).size.width; + double height = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: false, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + Stack( + children: [ + SizedBox( + width: double.infinity, + height: height * 0.4, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch( + profilegetvendorProvider, + ); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + + if (profile != null && + !_isProfileDataInitialized) { + WidgetsBinding.instance + .addPostFrameCallback((_) { + setState(() { + nameController.text = + profile.name ?? ''; + numberController.text = + profile.number ?? ''; + emailController.text = + profile.email ?? ''; + addressController.text = + profile.address ?? ''; + _isProfileDataInitialized = true; + }); + }); + } + + return _image == null + ? _buildNetworkImage( + profile?.profilePic1, + ) + : Image.file( + _image!, + fit: BoxFit.cover, + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, _) => Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Icon(Icons.error, size: 50), + Text('Error loading profile: $error'), + ], + ), + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + child: CustomPaint( + size: Size(width, height * 0.4), + painter: RightToLeftFullWidthTrianglePainter(), + ), + ), + Positioned( + bottom: height * 0.03, + left: width / 2 - 170, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch( + profilegetvendorProvider, + ); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + return CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: + _getAvatarImageProvider( + profile?.profilePic1, + ), + onBackgroundImageError: + (error, stackTrace) { + print( + 'Avatar image error: $error', + ); + }, + ), + ); + }, + loading: () => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + child: CircularProgressIndicator(), + ), + ), + error: (error, _) => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: AssetImage( + AppAssets.profile, + ), + ), + ), + ); + }, + ), + ), + Positioned( + bottom: height * 0.08, + left: width / 2 - 20, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch( + profilegetvendorProvider, + ); + return profileData.when( + data: (profiles) { + final profile = profiles.isNotEmpty + ? profiles[0] + : null; + if (profile == null) { + return Text( + 'No Profile Data', + style: gilroyTextStyle, + ); + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + profile.name ?? 'No Name', + style: gilroyTextStyle, + ), + IconButton( + onPressed: _pickImage, + icon: const Icon(Icons.edit), + ), + ], + ), + Row( + children: [ + const Icon( + Icons.location_city, + size: 20, + ), + const SizedBox(width: 4), + Text( + profile.address ?? + 'No address available', + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ), + ], + ); + }, + loading: () => + const CircularProgressIndicator(), + error: (error, _) => Text( + 'Error: $error', + style: gilroyTextStyle, + ), + ); + }, + ), + ), + ], + ), + const SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: width * 0.05), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Name", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your name", + controller: nameController, + ), + const SizedBox(height: 30), + Text("Mobile Number", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your mobile number", + keyboardType: TextInputType.phone, + controller: numberController, + ), + const SizedBox(height: 30), + Text("Address", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your address", + controller: addressController, + ), + const SizedBox(height: 30), + Text("Email", style: gilroyTextStyle), + const SizedBox(height: 15), + CommonTextFormField( + hintText: "Enter your email", + keyboardType: TextInputType.emailAddress, + controller: emailController, + ), + const SizedBox(height: 30), + Center( + child: Consumer( + builder: (context, ref, _) { + return CommanButton( + width: width * 0.6, + text: "Save", + textStyle: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 23, + height: 1.0, + letterSpacing: 0.01, + color: AppColors.secondprimary, + ), + onPressed: () async { + try { + final repo = ref.read( + profileupdatevendorRepositoryProvider, + ); + await repo.updateProfilevendor( + url: ConstsApi.upadatevendorprofile, + name: nameController.text, + number: numberController.text, + email: emailController.text, + address: addressController.text, + imageFile: _image, + ); + + if (mounted) { + Fluttertoast.showToast( + msg: "Profile updated successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + + ref.refresh(profilegetvendorProvider); + + Future.delayed( + const Duration(milliseconds: 1500), + () { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: "Error updating profile: $e", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + }, + ); + }, + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } +} + +class RightToLeftFullWidthTrianglePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white; + final path = Path(); + path.moveTo(size.width, 0); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/view/vendor_main_screens/Vendor_profile/vendor_profile_main.dart b/lib/view/vendor_main_screens/Vendor_profile/vendor_profile_main.dart new file mode 100644 index 0000000..a1eedc3 --- /dev/null +++ b/lib/view/vendor_main_screens/Vendor_profile/vendor_profile_main.dart @@ -0,0 +1,833 @@ +import 'dart:io'; +import 'dart:ui'; + + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; + +import 'package:image_picker/image_picker.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class VendorProfileMain extends ConsumerStatefulWidget { + const VendorProfileMain({super.key}); + + @override + ConsumerState createState() => _VendorProfileMainState(); +} + +class _VendorProfileMainState extends ConsumerState { + File? _image; + final ImagePicker _picker = ImagePicker(); + bool _isProfileDataInitialized = false; + bool _isUploading = false; + + final TextStyle gilroyTextStyle = const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.45, + letterSpacing: 0.01, + ); + + // Modified _pickImage method with automatic API upload using existing repository + Future _pickImage() async { + try { + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + _image = File(pickedFile.path); + _isUploading = true; + }); + + // Automatically upload to API using existing repository + await _uploadImageToAPI(File(pickedFile.path)); + } + } catch (e) { + debugPrint('Error picking image: $e'); + _showErrorToast('Failed to pick image: $e'); + } + } + + // Upload image using existing ProfileupdateRepository + Future _uploadImageToAPI(File imageFile) async { + try { + // Get current profile data + final profileData = ref.read(profilegetvendorProvider); + + await profileData.when( + data: (profiles) async { + if (profiles.isNotEmpty) { + final profile = profiles[0]; + + // Get the repository + final repository = ref.read(profileupdatevendorRepositoryProvider); + + // Update profile with new image and existing data + await repository.updateProfilevendor( + url: ConstsApi.upadatevendorprofile, + name: profile.name ?? '', + number: profile.number ?? '', + email: profile.email ?? '', + address: profile.address ?? '', + imageFile: imageFile, + ); + + // Refresh profile data to get updated image URL + ref.invalidate(profilegetvendorProvider); + + _showSuccessToast('Profile picture updated successfully!'); + + // Clear local image since it's now uploaded + if (mounted) { + setState(() { + _image = null; + }); + } + } else { + _showErrorToast('No profile data available'); + if (mounted) { + setState(() { + _image = null; + }); + } + } + }, + loading: () async { + _showErrorToast('Profile data is still loading'); + if (mounted) { + setState(() { + _image = null; + }); + } + }, + error: (error, _) async { + _showErrorToast('Error getting profile data: $error'); + if (mounted) { + setState(() { + _image = null; + }); + } + }, + ); + } catch (e) { + debugPrint('Error uploading image: $e'); + _showErrorToast('Upload failed: $e'); + if (mounted) { + setState(() { + _image = null; + }); + } + } finally { + if (mounted) { + setState(() { + _isUploading = false; + }); + } + } + } + + // Helper methods for showing toasts + void _showSuccessToast(String message) { + if (mounted) { + Fluttertoast.showToast( + msg: message, + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + } + + void _showErrorToast(String message) { + if (mounted) { + Fluttertoast.showToast( + msg: message, + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + } + + // Helper method to validate and construct image URL + String? _getValidImageUrl(String? imageUrl) { + if (imageUrl == null || imageUrl.isEmpty) { + return null; + } + + // Check if URL is complete (has a filename) + if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) { + return null; + } + + // Ensure URL is properly formatted + if (!imageUrl.startsWith('http')) { + return null; + } + + return imageUrl; + } + + // Helper widget for safe network image loading + Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) { + final validUrl = _getValidImageUrl(imageUrl); + + if (validUrl == null) { + return Image.asset(AppAssets.profile, fit: fit); + } + + return Image.network( + validUrl, + fit: fit, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: + loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + debugPrint('Error loading image: $error'); + return Image.asset(AppAssets.profile, fit: fit); + }, + ); + } + + // Helper method for CircleAvatar image provider + ImageProvider _getAvatarImageProvider(String? imageUrl) { + if (_image != null) { + return FileImage(_image!); + } + + final validUrl = _getValidImageUrl(imageUrl); + if (validUrl != null) { + return NetworkImage(validUrl); + } + + return const AssetImage(AppAssets.profile); + } + + void _showSignOutDialog() { + if (!mounted) return; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Sign Out'), + content: const Text('Are you sure you want to sign out?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('userId'); + await prefs.remove('vendor_id'); + await prefs.remove('data'); + + if (context.mounted) { + // context.go( + // RouterConts.loginpage, + // ); // or use pushReplacement if needed + } + }, + child: const Text('Sign Out'), + ), + ], + ); + }, + ); + } + + Widget _buildMenuItem(IconData icon, String title) { + return ListTile( + leading: Icon( + icon, + size: 23.000513076782227, + color: const Color(0xFF3E3E3F), + ), + title: Text( + title, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 18, + height: 1.0, + letterSpacing: 0.2, + color: Color(0xFF3E3E3F), + ), + ), + trailing: const Icon(Icons.arrow_forward_ios_rounded), + onTap: () async { + try { + if (title == 'My Profile') { + await Get.toNamed(RouterConts.vendoreditprofile); + } else if (title == 'Change Password') { + // await context.push(RouterConts.changepassword); + } else if (title == 'Sign out') { + _showSignOutDialog(); + } else if (title == 'Terms and Conditions') { + _showTermsAndConditionsDialog(ref); // Call the new dialog + } else if (title == 'Privacy policy') { + _showprivacyDialog(ref); // Call the new dialog + } + // Add other navigation cases as needed + } catch (e) { + debugPrint('Navigation error: $e'); + if (mounted) { + _showErrorToast('Navigation error: $e'); + } + } + }, + ); + } + + @override + Widget build(BuildContext context) { + double width = MediaQuery.of(context).size.width; + double height = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: SafeArea( + child: Column( + children: [ + Flexible( + flex: 1, + child: Stack( + children: [ + SizedBox( + width: double.infinity, + height: height * 0.4, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch(profilegetvendorProvider); + return profileData.when( + data: (profiles) { + final profile = + profiles.isNotEmpty ? profiles[0] : null; + + if (profile != null && !_isProfileDataInitialized) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isProfileDataInitialized = true; + }); + } + }); + } + + return _image == null + ? _buildNetworkImage(profile?.profilePic1) + : Image.file(_image!, fit: BoxFit.cover); + }, + loading: + () => const Center( + child: CircularProgressIndicator(), + ), + error: + (error, _) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, size: 50), + Text('Error loading profile: $error'), + ], + ), + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + child: CustomPaint( + size: Size(width, height * 0.4), + painter: RightToLeftFullWidthTrianglePainter(), + ), + ), + Positioned( + bottom: height * 0.03, + left: width / 2 - 170, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch(profilegetvendorProvider); + return profileData.when( + data: (profiles) { + final profile = + profiles.isNotEmpty ? profiles[0] : null; + return Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: _getAvatarImageProvider( + profile?.profilePic1, + ), + onBackgroundImageError: ( + error, + stackTrace, + ) { + debugPrint('Avatar image error: $error'); + }, + ), + ), + // Show upload progress indicator + if (_isUploading) + Container( + width: 136, + height: 136, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ), + ), + ], + ); + }, + loading: + () => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + child: CircularProgressIndicator(), + ), + ), + error: + (error, _) => const CircleAvatar( + radius: 68, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 65, + backgroundImage: AssetImage( + AppAssets.profile, + ), + ), + ), + ); + }, + ), + ), + Positioned( + bottom: height * 0.08, + left: width / 2 - 20, + child: Consumer( + builder: (context, ref, _) { + final profileData = ref.watch(profilegetvendorProvider); + return profileData.when( + data: (profiles) { + final profile = + profiles.isNotEmpty ? profiles[0] : null; + if (profile == null) { + return Text( + 'No Profile Data', + style: gilroyTextStyle, + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + profile.name ?? 'No Name', + style: gilroyTextStyle, + ), + IconButton( + onPressed: + _isUploading ? null : _pickImage, + icon: + _isUploading + ? const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.edit), + ), + ], + ), + Row( + children: [ + const Icon(Icons.location_city, size: 20), + const SizedBox(width: 4), + Text( + profile.address ?? 'No address available', + style: const TextStyle(fontSize: 12), + ), + ], + ), + ], + ); + }, + loading: () => const CircularProgressIndicator(), + error: + (error, _) => + Text('Error: $error', style: gilroyTextStyle), + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 30), + Expanded( + flex: 1, + child: ListView( + shrinkWrap: true, + children: [ + _buildMenuItem(Icons.person, 'My Profile'), + + _buildMenuItem(Icons.notifications, 'Notification'), + _buildMenuItem(Icons.safety_check, 'Privacy policy'), + _buildMenuItem(Icons.description, 'Terms and Conditions'), + _buildMenuItem(Icons.logout, 'Sign out'), + ], + ), + ), + ], + ), + ), + ); + } + + void _showTermsAndConditionsDialog(WidgetRef ref) { + if (!mounted) return; + + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Center( + child: Consumer( + builder: (context, ref, _) { + final termsAsync = ref.watch(termsAndConditionsProvider); + + return termsAsync.when( + data: + (terms) => Container( + width: 389, + height: 547, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFF959595), + width: 1, + ), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Text( + "Terms & Condition", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + const SizedBox(height: 12), + Expanded( + child: ScrollbarTheme( + data: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all( + AppColors.primary, + ), + trackColor: WidgetStateProperty.all( + Colors.transparent, + ), + trackBorderColor: WidgetStateProperty.all( + Colors.transparent, + ), + thickness: WidgetStateProperty.all(13), + radius: const Radius.circular(19), + thumbVisibility: WidgetStateProperty.all( + true, + ), + ), + child: Scrollbar( + thumbVisibility: true, + interactive: true, + thickness: 13, + radius: const Radius.circular(19), + child: ListView( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + terms.data, + style: const TextStyle( + color: Colors.black, + ), + ), + const SizedBox(height: 20), + Center( + child: SizedBox( + width: 228, + height: 51.82, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: + const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 22.95, + ), + ), + ), + child: const Text( + "OK", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Poppins', + fontWeight: + FontWeight.w500, + fontSize: 18.87, + height: 23.59 / 18.87, + letterSpacing: -0.59, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ), + loading: + () => const Center(child: CircularProgressIndicator()), + error: + (e, _) => Container( + padding: const EdgeInsets.all(16), + child: Text( + "Error: ${e.toString()}", + style: const TextStyle(color: Colors.red), + ), + ), + ); + }, + ), + ), + ), + ); + }, + ); + } + + void _showprivacyDialog(WidgetRef ref) { + if (!mounted) return; + + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Center( + child: Consumer( + builder: (context, ref, _) { + final privacyAsync = ref.watch(privacypolicyProvider); + + return privacyAsync.when( + data: + (terms) => Container( + width: 389, + height: 547, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFF959595), + width: 1, + ), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Text( + "Privacy policy", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + const SizedBox(height: 12), + Expanded( + child: ScrollbarTheme( + data: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all( + AppColors.primary, + ), + trackColor: WidgetStateProperty.all( + Colors.transparent, + ), + trackBorderColor: WidgetStateProperty.all( + Colors.transparent, + ), + thickness: WidgetStateProperty.all(13), + radius: const Radius.circular(19), + thumbVisibility: WidgetStateProperty.all( + true, + ), + ), + child: Scrollbar( + thumbVisibility: true, + interactive: true, + thickness: 13, + radius: const Radius.circular(19), + child: ListView( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + terms.data, + style: const TextStyle( + color: Colors.black, + ), + ), + const SizedBox(height: 20), + Center( + child: SizedBox( + width: 228, + height: 51.82, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: + const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 22.95, + ), + ), + ), + child: const Text( + "OK", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Poppins', + fontWeight: + FontWeight.w500, + fontSize: 18.87, + height: 23.59 / 18.87, + letterSpacing: -0.59, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ), + loading: + () => const Center(child: CircularProgressIndicator()), + error: + (e, _) => Container( + padding: const EdgeInsets.all(16), + child: Text( + "Error: ${e.toString()}", + style: const TextStyle(color: Colors.red), + ), + ), + ); + }, + ), + ), + ), + ); + }, + ); + } +} + +class RightToLeftFullWidthTrianglePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white; + + final path = Path(); + path.moveTo(size.width, 0); // top right corner + path.lineTo(size.width, size.height); // bottom right corner + path.lineTo(0, size.height); // bottom left (tip) + path.close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/view/vendor_main_screens/vendor_catgories.dart b/lib/view/vendor_main_screens/vendor_catgories.dart new file mode 100644 index 0000000..86d084a --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_catgories.dart @@ -0,0 +1,1017 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/model/vendor_model/vendor_serviceupload_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class VendorCatgories extends ConsumerStatefulWidget { + const VendorCatgories({super.key}); + + @override + ConsumerState createState() => _VendorCatgoriesState(); +} + +class _VendorCatgoriesState extends ConsumerState { + String? selectedCategory; + String? selectedSubCategory; + String? selectedWorkingHours; + String? selectedWorkingDuration; + String? selectedServiceType; + List selectedImages = []; + List videoLinks = []; + TextEditingController videoLinkController = TextEditingController(); + TextEditingController nameController = TextEditingController(); + TextEditingController serviceNameController = TextEditingController(); + TextEditingController serviceAmountController = TextEditingController(); + TextEditingController serviceDetailsController = TextEditingController(); + TextEditingController locationController = TextEditingController(); + TextEditingController descriptionController = TextEditingController(); + + // Validation error messages + String? nameError; + String? serviceNameError; + String? categoryError; + String? subCategoryError; + String? workingHoursError; + String? workingDurationError; + String? serviceTypeError; + String? serviceAmountError; + String? serviceDetailsError; + String? locationError; + String? imagesError; + + List categories = []; + List subcategories = []; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _fetchCategories(); + + // Add listeners to clear validation errors when typing + nameController.addListener(() { + if (nameError != null && nameController.text.isNotEmpty) { + setState(() => nameError = null); + } + }); + + serviceNameController.addListener(() { + if (serviceNameError != null && serviceNameController.text.isNotEmpty) { + setState(() => serviceNameError = null); + } + }); + + serviceAmountController.addListener(() { + if (serviceAmountError != null && + serviceAmountController.text.isNotEmpty) { + setState(() => serviceAmountError = null); + } + }); + + serviceDetailsController.addListener(() { + if (serviceDetailsError != null && + serviceDetailsController.text.isNotEmpty) { + setState(() => serviceDetailsError = null); + } + }); + + locationController.addListener(() { + if (locationError != null && locationController.text.isNotEmpty) { + setState(() => locationError = null); + } + }); + + // Add listener for description controller even though it's optional + descriptionController.addListener(() { + if (descriptionController.text.isNotEmpty) { + // No error for description as it's optional, but added for consistency + } + }); + + // Add listener for video link controller + videoLinkController.addListener(() { + // No error for video links as they're optional, but added for consistency + }); + } + + @override + void dispose() { + nameController.dispose(); + serviceNameController.dispose(); + serviceAmountController.dispose(); + serviceDetailsController.dispose(); + locationController.dispose(); + descriptionController.dispose(); + videoLinkController.dispose(); + super.dispose(); + } + + Future _fetchCategories() async { + try { + final categoryData = await ref.read(categoryListProvider.future); + setState(() { + categories = categoryData; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + print('Error fetching categories: $e'); + } + } + + Future _fetchSubcategories(String categoryId) async { + setState(() { + isLoading = true; + subcategories = []; + selectedSubCategory = + null; // Reset selected subcategory when category changes + subCategoryError = null; // Clear subcategory error when fetching new ones + }); + + try { + final subcategoryData = await ref.read( + subcategoryProvider(categoryId).future, + ); + setState(() { + subcategories = subcategoryData; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + print('Error fetching subcategories: $e'); + } + } + + Future _pickImages() async { + final picker = ImagePicker(); + final pickedFiles = await picker.pickMultiImage(); + + if (pickedFiles.isNotEmpty) { + List validImages = []; + + for (final pickedFile in pickedFiles) { + final file = File(pickedFile.path); + final bytes = await file.length(); // get size in bytes + final sizeInMB = bytes / (1024 * 1024); // convert to MB + + if (sizeInMB <= 2) { + validImages.add(file); + } + } + + if (validImages.isEmpty) { + setState(() { + imagesError = 'Each image must be less than 2MB'; + }); + } else { + setState(() { + selectedImages.addAll(validImages); + imagesError = null; + }); + } + } + } + + void _addVideoLink() { + if (videoLinkController.text.trim().isNotEmpty) { + setState(() { + videoLinks.add(videoLinkController.text.trim()); + videoLinkController.clear(); + }); + } + } + + void _removeImage(int index) { + setState(() { + selectedImages.removeAt(index); + // Re-validate images if list becomes empty + if (selectedImages.isEmpty) { + imagesError = 'Please upload at least one image'; + } + }); + } + + void _removeVideoLink(int index) { + setState(() { + videoLinks.removeAt(index); + }); + } + + bool _validateForm() { + bool isValid = true; + + if (nameController.text.isEmpty) { + setState(() => nameError = 'Please enter your name'); + isValid = false; + } + + if (serviceNameController.text.isEmpty) { + setState(() => serviceNameError = 'Please enter service name'); + isValid = false; + } + + if (selectedCategory == null) { + setState(() => categoryError = 'Please select a category'); + isValid = false; + } + + if (selectedSubCategory == null) { + setState(() => subCategoryError = 'Please select a sub-category'); + isValid = false; + } + + if (selectedWorkingHours == null) { + setState(() => workingHoursError = 'Please select working hours'); + isValid = false; + } + + if (selectedWorkingDuration == null) { + setState(() => workingDurationError = 'Please select working duration'); + isValid = false; + } + + if (selectedServiceType == null) { + setState(() => serviceTypeError = 'Please select service type'); + isValid = false; + } + + // if (serviceAmountController.text.isEmpty) { + // setState(() => serviceAmountError = 'Please enter service amount'); + // isValid = false; + // } + + if (serviceDetailsController.text.isEmpty) { + setState(() => serviceDetailsError = 'Please enter service details'); + isValid = false; + } + + if (locationController.text.isEmpty) { + setState(() => locationError = 'Please enter location'); + isValid = false; + } + + if (selectedImages.isEmpty) { + setState(() => imagesError = 'Please upload at least one image'); + isValid = false; + } + + return isValid; + } + + List generateWorkingHours() { + List workingHours = []; + TimeOfDay startTime = TimeOfDay(hour: 9, minute: 0); + TimeOfDay endTime = TimeOfDay(hour: 18, minute: 0); + + while (startTime.hour < endTime.hour || + (startTime.hour == endTime.hour && + startTime.minute <= endTime.minute)) { + final hour = startTime.hourOfPeriod.toString().padLeft(2, '0'); + final minute = startTime.minute.toString().padLeft(2, '0'); + final period = startTime.period == DayPeriod.am ? 'am' : 'pm'; + workingHours.add('$hour:$minute $period'); + + int newMinute = startTime.minute + 30; + int newHour = startTime.hour; + if (newMinute >= 60) { + newMinute -= 60; + newHour += 1; + } + startTime = TimeOfDay(hour: newHour, minute: newMinute); + } + + return workingHours; + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final keyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0; + + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: false, + body: SingleChildScrollView( + padding: EdgeInsets.only(bottom: keyboardOpen ? 20 : 0), + child: Column( + children: [ + Image.asset( + AppAssets.login, + width: screenSize.width, + height: screenSize.height * 0.45, + fit: BoxFit.cover, + ), + Transform.translate( + offset: const Offset(0, -100), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: screenSize.width * 0.05, + ), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.73), + border: Border.all( + width: 1.09, + color: const Color(0xFFA39898), + ), + boxShadow: const [ + BoxShadow( + offset: Offset(0, -4), + blurRadius: 12.3, + color: Color(0x66E0E0E0), + ), + BoxShadow( + offset: Offset(0, 4), + blurRadius: 4, + color: Color(0x66000000), + ), + ], + color: Colors.white, + ), + child: Padding( + padding: EdgeInsets.all(screenSize.width * 0.05), + child: Column( + children: [ + const Text( + "Service Upload", + style: TextStyle( + fontSize: 25, + fontFamily: "Gilroy-ExtraBold", + fontWeight: FontWeight.w800, + height: 1.0, + letterSpacing: 0.64, + ), + ), + const SizedBox(height: 25), + CommonTextFormField( + controller: nameController, + hintText: 'Enter your name', + errorText: nameError, + prefixIcon: const Icon( + Icons.person, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + CommonTextFormField( + controller: serviceNameController, + hintText: 'Service name', + errorText: serviceNameError, + prefixIcon: const Icon( + Icons.build, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Categories', + Icons.category, + errorText: categoryError, + ), + value: selectedCategory, + items: _buildCategoryItems(), + onChanged: (value) { + setState(() { + selectedCategory = value; + selectedSubCategory = null; + categoryError = null; + }); + if (value != null) _fetchSubcategories(value); + }, + isExpanded: true, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Sub-categories', + Icons.list, + errorText: subCategoryError, + ), + value: selectedSubCategory, + items: _buildSubcategoryItems(), + onChanged: subcategories.isNotEmpty + ? (value) { + setState(() { + selectedSubCategory = value; + subCategoryError = null; + }); + } + : null, + isExpanded: true, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Working Hours', + Icons.access_time, + errorText: workingHoursError, + ), + value: selectedWorkingHours, + items: generateWorkingHours() + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + selectedWorkingHours = value; + workingHoursError = null; + }); + }, + isExpanded: true, + menuMaxHeight: 300, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Working Duration', + Icons.timelapse, + errorText: workingDurationError, + ), + value: selectedWorkingDuration, + items: + [ + '01:00 hours', + '02:00 hours', + '03:00 hours', + '04:00 hours', + '05:00 hours', + '06:00 hours', + ] + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + selectedWorkingDuration = value; + workingDurationError = null; + }); + }, + isExpanded: true, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Service Free or Paid', + Icons.money, + errorText: serviceTypeError, + ), + value: selectedServiceType, + items: ['Free Service', 'Paid Service'] + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + selectedServiceType = value; + serviceTypeError = null; + // Clear the amount when switching to free service + if (value == 'Free Service') { + serviceAmountController.clear(); + serviceAmountError = null; + } + }); + }, + isExpanded: true, + ), + const SizedBox(height: 20), + // Conditionally show the service amount field + if (selectedServiceType == 'Paid Service') ...[ + CommonTextFormField( + controller: serviceAmountController, + hintText: 'Service Amount', + errorText: serviceAmountError, + prefixIcon: const Icon( + Icons.currency_rupee, + color: AppColors.hittext, + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 20), + ], + CommonTextFormField( + controller: serviceDetailsController, + hintText: 'Service Details', + errorText: serviceDetailsError, + prefixIcon: const Icon( + Icons.description, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + CommonTextFormField( + controller: locationController, + hintText: 'Location', + errorText: locationError, + prefixIcon: const Icon( + Icons.location_on, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + CommonTextFormField( + controller: descriptionController, + hintText: 'Description (Optional)', + prefixIcon: Padding( + padding: const EdgeInsets.only(bottom: 48), + child: Icon( + Icons.description, + color: AppColors.hittext, + ), + ), + maxLines: 5, + minLines: 3, + keyboardType: TextInputType.multiline, + ), + const SizedBox(height: 20), + // Image Upload Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: _pickImages, + child: Container( + width: 349, + height: 56.12, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.78), + border: Border.all( + color: const Color(0xFFBDBCBC), + width: 0.88, + ), + ), + child: const Center( + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.image, + color: AppColors.hittext, + ), + SizedBox(width: 10), + Text( + 'Upload Images', + style: TextStyle( + color: AppColors.hittext, + ), + ), + ], + ), + ), + ), + ), + if (imagesError != null) + Padding( + padding: const EdgeInsets.only( + left: 16, + top: 8, + ), + child: Text( + imagesError!, + style: TextStyle( + color: Colors.red[700], + fontSize: 12, + ), + ), + ), + ], + ), + + // Display selected images + if (selectedImages.isNotEmpty) ...[ + const SizedBox(height: 10), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: selectedImages.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Stack( + children: [ + Container( + width: 90, + height: 90, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 16, + ), + image: DecorationImage( + image: FileImage( + selectedImages[index], + ), + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => _removeImage(index), + child: AnimatedContainer( + duration: const Duration( + milliseconds: 200, + ), + curve: Curves.easeInOut, + width: 20, + height: 18, + decoration: BoxDecoration( + color: Colors.red.withOpacity( + 0.8, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.white, + size: 13, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + const SizedBox(height: 20), + // Video Links Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: CommonTextFormField( + controller: videoLinkController, + hintText: + 'Add YouTube video link (Optional)', + prefixIcon: const Icon( + Icons.video_library, + color: AppColors.hittext, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.add, + color: AppColors.primary, + ), + onPressed: _addVideoLink, + ), + ], + ), + const SizedBox(height: 4), + const Text( + 'Optional - Add videos of your service', + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + // Display added video links + if (videoLinks.isNotEmpty) ...[ + const SizedBox(height: 10), + Column( + children: videoLinks.map((link) { + return ListTile( + leading: const Icon( + Icons.video_library, + color: AppColors.primary, + ), + title: Text( + link, + maxLines: 1, + style: const TextStyle(fontSize: 13), + ), + trailing: IconButton( + icon: const Icon( + Icons.close, + size: 23, + color: Colors.red, + ), + onPressed: () => _removeVideoLink( + videoLinks.indexOf(link), + ), + ), + ); + }).toList(), + ), + ], + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () async { + if (_validateForm()) { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center( + child: CircularProgressIndicator( + color: AppColors.primary, + ), + ), + ); + + try { + // Get user ID from shared preferences or other storage + final prefs = + await SharedPreferences.getInstance(); + final dataid = prefs.getString('vendor_id'); + + // Create the model + final model = VendorServiceUploadModel( + id: dataid.toString(), + vendorName: nameController.text, + serviceName: serviceNameController.text, + serviceType: + selectedServiceType == 'Free Service' + ? '0' + : '1', + category: selectedCategory!, + subcategory: selectedSubCategory!, + workingHours: selectedWorkingHours!, + workingDuration: selectedWorkingDuration!, + amount: + double.tryParse( + serviceAmountController.text, + ) ?? + 0.0, + location: locationController.text, + description: descriptionController.text, + details: serviceDetailsController.text, + images: + [], // We'll pass actual images separately + videos: videoLinks, + ); + + // Call the API using Riverpod with both model and images + final result = await ref.read( + vendorserviceFutureProvider(( + model, + selectedImages, + )).future, + ); + + // Close loading dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + // Check if the result indicates success + if (result != null && + (result['success'] == true || + result.containsKey('data') && + result['success'] != false)) { + // Show success message + Fluttertoast.showToast( + msg: + result['message'] ?? + "Service uploaded successfully", + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + + // Clear the form only after successful upload + _clearForm(); + + // Navigate back if needed + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + } else { + // Handle API failure response + String errorMessage = + result?['error'] ?? + result?['message'] ?? + "Upload failed. Please try again."; + + Fluttertoast.showToast( + msg: errorMessage, + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + + if (errorMessage == + 'Supscription not found') { + Get.toNamed( + RouterConts.vendorpackage, + arguments: 1, + ); + } + } + } catch (e) { + // Close loading dialog safely + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + // Show error message + Fluttertoast.showToast( + msg: "Error: ${e.toString()}", + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + + // Don't clear form or navigate on error + } + } + }, + child: const Text( + 'Submit', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + SizedBox(height: 30), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + List> _buildCategoryItems() { + if (categories.isEmpty) { + return [ + const DropdownMenuItem( + value: null, + child: Text('No categories available'), + ), + ]; + } + return categories.map>((cat) { + try { + final id = cat.id?.toString() ?? ''; + final name = cat.name?.toString() ?? 'Unknown'; + return DropdownMenuItem(value: id, child: Text(name)); + } catch (e) { + print('Error: $e'); + return const DropdownMenuItem( + value: null, + child: Text('Error'), + ); + } + }).toList(); + } + + List> _buildSubcategoryItems() { + if (subcategories.isEmpty) { + return [ + DropdownMenuItem( + value: null, + child: Text( + selectedCategory == null + ? 'Select a category first' + : isLoading + ? 'Loading subcategories...' + : 'No subcategories available', + ), + ), + ]; + } + return subcategories.map>((sub) { + try { + final id = sub.id?.toString() ?? ''; + final name = sub.name?.toString() ?? 'Unknown'; + return DropdownMenuItem(value: id, child: Text(name)); + } catch (e) { + print('Error: $e'); + return const DropdownMenuItem( + value: null, + child: Text('Error'), + ); + } + }).toList(); + } + + InputDecoration _buildDropdownDecoration( + String hintText, + IconData icon, { + String? errorText, + }) { + return InputDecoration( + filled: true, + fillColor: Colors.white, + prefixIcon: Icon(icon, color: AppColors.hittext), + hintText: hintText, + hintStyle: const TextStyle(color: AppColors.hittext), + errorText: errorText, + errorStyle: TextStyle(color: Colors.red[700], fontSize: 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: const BorderSide(color: Color(0xFFBDBCBC), width: 0.88), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: const BorderSide(color: Color(0xFFBDBCBC), width: 0.88), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: BorderSide(color: Colors.red[700]!, width: 0.88), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: BorderSide(color: Colors.red[700]!, width: 0.88), + ), + ); + } + + void _clearForm() { + setState(() { + // Clear text controllers + nameController.clear(); + serviceNameController.clear(); + serviceAmountController.clear(); + locationController.clear(); + descriptionController.clear(); + serviceDetailsController.clear(); + + // Reset dropdown selections + selectedServiceType = null; + selectedCategory = null; + selectedSubCategory = null; + selectedWorkingHours = null; + selectedWorkingDuration = null; + + // Clear images and videos + selectedImages.clear(); + videoLinks.clear(); + + // Clear any error states if you have them + imagesError = null; + // Add other error variables if you have them + }); + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/category_management.dart b/lib/view/vendor_main_screens/vendor_history/category_management.dart new file mode 100644 index 0000000..10c99ea --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/category_management.dart @@ -0,0 +1,516 @@ +import 'dart:ui'; + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; + +final selectedCategoryIndexProvider = StateProvider((ref) => null); +final deleteLoadingProvider = StateProvider((ref) => false); + +class CategoryManagement extends ConsumerStatefulWidget { + const CategoryManagement({super.key}); + + @override + ConsumerState createState() => _CategoryManagementState(); +} + +class _CategoryManagementState extends ConsumerState { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Future _showDeleteConfirmation( + BuildContext context, + WidgetRef ref, + String categoryId, + String categoryName, + ) async { + final confirmed = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Category'), + content: Text('Are you sure you want to delete "$categoryName"?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (confirmed == true && context.mounted) { + await _deleteCategory(context, ref, categoryId); + } + } + + Future _deleteCategory( + BuildContext context, + WidgetRef ref, + String categoryId, + ) async { + try { + ref.read(deleteLoadingProvider.notifier).state = true; + + final repo = ref.read(catDeleteRepositoryProvider); + final success = await repo.deleteCategoryManagement( + ConstsApi.catmangementdelete, + categoryId, + ); + + ref.read(deleteLoadingProvider.notifier).state = false; + + if (success) { + ref.read(selectedCategoryIndexProvider.notifier).state = null; + ref.invalidate(vendorcatgoriesProvider); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Category deleted successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to delete category'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + ref.read(deleteLoadingProvider.notifier).state = false; + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final categoryAsyncValue = ref.watch(vendorcatgoriesProvider); + final selectedIndex = ref.watch(selectedCategoryIndexProvider); + final isDeleting = ref.watch(deleteLoadingProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Stack( + children: [ + categoryAsyncValue.when( + data: (categories) => + _buildSuccessUI(categories, selectedIndex, isDeleting), + loading: () => _buildLoadingUI(), + error: (error, _) => _buildErrorUI(error), + ), + if (isDeleting) _buildDeleteOverlay(), + ], + ), + ); + } + + Widget _buildSuccessUI(List categories, int? selectedIndex, bool isDeleting) { + final filteredCategories = categories + .where((c) => c.noOfService! > 0) + .toList(); + + return CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 196 / 209, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index >= filteredCategories.length) return null; + return _buildCategoryCard( + filteredCategories[index], + index, + selectedIndex, + isDeleting, + ); + }, + childCount: filteredCategories.length, + addAutomaticKeepAlives: false, + addRepaintBoundaries: true, + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 20)), + ], + ); + } + + Widget _buildCategoryCard( + dynamic category, + int index, + int? selectedIndex, + bool isDeleting, + ) { + final isSelected = selectedIndex == index; + + return RepaintBoundary( + child: GestureDetector( + onTap: () { + final current = ref.read(selectedCategoryIndexProvider); + ref.read(selectedCategoryIndexProvider.notifier).state = + current == index ? null : index; + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: AspectRatio( + aspectRatio: 196 / 209, + child: Stack( + fit: StackFit.expand, + children: [ + // Category Image + _buildCategoryImage(category.image1.toString()), + + // Action Buttons (Edit/Delete) + if (isSelected) _buildActionButtons(category, isDeleting), + + // Name Overlay with Blur + _buildImageOverlay(category.name.toString()), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildCategoryImage(String imageUrl) { + return Image.network( + imageUrl, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: Colors.grey[300], + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) => Container( + color: Colors.grey[300], + child: const Center( + child: Icon(Icons.image_not_supported, color: Colors.grey, size: 32), + ), + ), + cacheWidth: 400, + cacheHeight: 400, + ); + } + + Widget _buildActionButtons(dynamic category, bool isDeleting) { + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Edit Button + GestureDetector( + onTap: () { + Get.toNamed( + RouterConts.vendorservice, + arguments: { + 'id': category.id, + 'tittle': category + .name, // Note: using 'tittle' to match VendorService parameter + }, + ); + }, + child: Container( + width: 57, + height: 57, + alignment: Alignment.center, + decoration: const BoxDecoration( + color: AppColors.secondprimary, + shape: BoxShape.circle, + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_document, color: AppColors.primary, size: 20), + Text( + "edit", + style: TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 10), + + // Delete Button + GestureDetector( + onTap: () { + _showDeleteConfirmation( + context, + ref, + category.id.toString(), + category.name.toString(), + ); + }, + child: Container( + width: 57, + height: 57, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDeleting ? Colors.grey : Colors.white, + shape: BoxShape.circle, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isDeleting ? Icons.hourglass_empty : Icons.delete, + color: Colors.red, + size: 20, + ), + Text( + isDeleting ? "wait" : "delete", + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildImageOverlay(String categoryName) { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + height: 56, + width: 196, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(255, 255, 255, 0.54), + Color.fromRGBO(153, 153, 153, 0.54), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Center( + child: Text( + categoryName, + style: const TextStyle( + color: Colors.black87, + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.2, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildLoadingUI() { + return CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 196 / 209, + ), + delegate: SliverChildBuilderDelegate( + (context, index) => _buildShimmerCard(), + childCount: 6, + ), + ), + ), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey[300], + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + Colors.grey[300]!, + Colors.grey[100]!, + Colors.grey[300]!, + ], + ), + ), + ), + Positioned( + bottom: 12, + left: 12, + right: 12, + child: Container( + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.grey[400], + ), + ), + ), + ], + ), + ); + } + + Widget _buildErrorUI(Object error) { + return CustomScrollView( + slivers: [ + SliverFillRemaining( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'Oops! Something went wrong', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + 'Please check your connection and try again', + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + ref.invalidate(vendorcatgoriesProvider); + }, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildDeleteOverlay() { + return Container( + color: Colors.black.withOpacity(0.5), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: Colors.white), + SizedBox(height: 16), + Text( + 'Deleting category...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_bookinglist.dart b/lib/view/vendor_main_screens/vendor_history/vendor_bookinglist.dart new file mode 100644 index 0000000..d52f1a6 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_bookinglist.dart @@ -0,0 +1,733 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/model/cancel_booking.dart'; +import 'package:bookmywages/model/vendor_model/vendor_booking_status.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:intl/intl.dart'; + +class VendorBookinglist extends ConsumerStatefulWidget { + const VendorBookinglist({super.key}); + + @override + ConsumerState createState() => _VendorBookinglistState(); +} + +class _VendorBookinglistState extends ConsumerState { + Map getStatusInfo(int status) { + switch (status) { + case 0: + return { + 'text': 'Pending', + 'color': Colors.orange, + 'bgColor': Colors.orange.shade100, + }; + case 1: + return { + 'text': 'Scheduled', + 'color': Colors.green, + 'bgColor': Colors.green.shade100, + }; + case 2: + return { + 'text': 'Completed', + 'color': Colors.blue, + 'bgColor': Colors.blue.shade100, + }; + case 3: + return { + 'text': 'Canceled', + 'color': Colors.red, + 'bgColor': Colors.red.shade100, + }; + default: + return { + 'text': 'pending', + 'color': Colors.blue, + 'bgColor': Colors.blue.shade100, + }; + } + } + + // Format date from API response + String formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + return DateFormat('MMMM dd, yyyy').format(date); + } catch (e) { + return dateStr; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ref + .watch(vendorbookingdetailsProvider) + .when( + data: (bookings) { + if (bookings.isEmpty) { + return const Center(child: Text('No bookings found')); + } + + return ListView.builder( + itemCount: bookings.length, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemBuilder: (context, index) { + final booking = bookings[index]; + final statusInfo = getStatusInfo(booking.status ?? 0); + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ID and View order + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + const Divider(), + + // Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (booking.images1 != null && + booking.images1!.isNotEmpty) + ? Image.network( + booking.images1!, + width: 100, + height: 110, + fit: BoxFit.cover, + errorBuilder: ( + context, + error, + stackTrace, + ) { + return Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 8, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + booking.name ?? 'No data', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + booking.serviceName ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 1.3, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container( + width: + booking.status == 3 + ? 70 + : booking.status == 1 + ? 85 + : 77.84, + height: + booking.status == 3 + ? 25 + : booking.status == 1 + ? 25 + : 27.96, + decoration: BoxDecoration( + color: + booking.status == 3 + ? const Color( + 0xFFFFEEEE, + ) + : booking.status == 1 + ? const Color( + 0xFFDAE9FF, + ) + : const Color( + 0xFFE6F7E6, + ), + borderRadius: + BorderRadius.circular(6.05), + ), + child: Center( + child: Text( + booking.status == 3 + ? 'Cancel' + : booking.status == 1 + ? 'completed' + : booking.status == 4 + ? 'Scheduled' + : 'pending', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: + booking.status == 3 + ? const Color( + 0xFFFF0000, + ) + : booking.status == + 1 + ? const Color( + 0xFF0066FF, + ) + : const Color( + 0xFF2E8B57, + ), + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? _formatDate( + booking.serviceDate!, + ) + : 'No date', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? DateFormat( + 'h:mm a', + ).format( + DateTime.parse( + booking.serviceDate!, + ), + ) + : 'No time', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + "Mobile number : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(width: 7), + Text( + booking.mobileNumber.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + const SizedBox(height: 13), + Row( + children: [ + const Text( + "E-mail ID :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(width: 7), + Text( + booking.email.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + const SizedBox(height: 13), + Row( + children: [ + const Text( + "Address : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(width: 7), + Text( + booking.address.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + const SizedBox(height: 13), + const Text( + "Message : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 13), + Text( + booking.message.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + if (booking.status == 0) + Padding( + padding: const EdgeInsets.all(8.0), + + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 42, + child: ElevatedButton( + onPressed: () async { + final scaffoldMessenger = + ScaffoldMessenger.of(context); + + try { + // Prepare data for status update + final data = VendorBookingStatus( + id: booking.id.toString(), + status: + "4", // Change to status 1 (confirmed) + ); + + // Call the provider to update status + await ref.read( + changebookingProvider(data).future, + ); + + Fluttertoast.showToast( + msg: + 'Booking confirmed successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + + // Refresh the booking list + ref.invalidate( + vendorbookingdetailsProvider, + ); + } catch (e) { + Fluttertoast.showToast( + msg: + 'Failed to confirm booking: $e', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF0066FF, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 20, + ), + ), + ), + child: const FittedBox( + child: Text( + "Confirmed", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w800, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.289, + color: Colors.white, + ), + ), + ), + ), + ), + GestureDetector( + onTap: () async { + final data = CancelBookingRequest( + id: booking.id.toString(), + serviceId: booking.serviceId.toString(), + type: + booking.type == null + ? "0" + : booking.type.toString(), + ); + + final scaffoldMessenger = + ScaffoldMessenger.of(context); + final navigator = Navigator.of(context); + + try { + await ref.read( + cancelbookingProvider(data).future, + ); + + Fluttertoast.showToast( + msg: 'Booking cancelled successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + + ref.invalidate( + vendorbookingdetailsProvider, + ); + } catch (e) { + Fluttertoast.showToast( + msg: 'Failed to cancel booking: $e', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + }, + child: const Text( + 'Cancel Booking', + style: TextStyle( + fontFamily: 'Gilroy-Bold', // Add this + fontWeight: + FontWeight + .w400, // 400 is normal weight + fontSize: 14.45, + height: + 13 / + 14.45, // Convert line-height to Flutter's height property + letterSpacing: 0.02, // 2% as decimal + color: Color(0xff534E4E), + ), + ), + ), + ], + ), + ) + else if (booking.status == 4) + Padding( + padding: const EdgeInsets.all(8.0), + + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 42, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF0066FF, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 20, + ), + ), + ), + child: const FittedBox( + child: Text( + "pending", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w800, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.289, + color: Colors.white, + ), + ), + ), + ), + ), + GestureDetector( + onTap: () async { + final scaffoldMessenger = + ScaffoldMessenger.of(context); + + try { + // Prepare data for status update + final data = VendorBookingStatus( + id: booking.id.toString(), + status: + "1", // Change to status 1 (confirmed) + ); + + // Call the provider to update status + await ref.read( + changebookingProvider(data).future, + ); + + Fluttertoast.showToast( + msg: 'Booking confirmed successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + + // Refresh the booking list + ref.invalidate( + vendorbookingdetailsProvider, + ); + } catch (e) { + Fluttertoast.showToast( + msg: 'Failed to confirm booking: $e', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + }, + child: const Text( + 'complete', + style: TextStyle( + fontFamily: 'Gilroy-Bold', // Add this + fontWeight: + FontWeight + .w400, // 400 is normal weight + fontSize: 14.45, + height: + 13 / + 14.45, // Convert line-height to Flutter's height property + letterSpacing: 0.02, // 2% as decimal + color: Color(0xff534E4E), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + ], + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: + (err, stack) => + Center(child: Text('Error loading bookings: $err')), + ), + ), + ); + } + + String _formatDate(String dateString) { + try { + return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString)); + } catch (e) { + return dateString; // Fallback: return the original string if parsing fails + } + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_cancel_booking.dart b/lib/view/vendor_main_screens/vendor_history/vendor_cancel_booking.dart new file mode 100644 index 0000000..cacea7e --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_cancel_booking.dart @@ -0,0 +1,420 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; + +class VendorCancelBooking extends ConsumerWidget { + // Changed to ConsumerWidget + const VendorCancelBooking({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Added WidgetRef parameter + final bookingAsyncValue = ref.watch(vendorbookingdetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: bookingAsyncValue.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text('Error: $err')), + data: (bookings) { + // Filter only canceled bookings (status == 3) + final canceledBookings = + bookings.where((booking) => booking.status == 3).toList(); + + if (canceledBookings.isEmpty) { + return const Center( + child: Text( + 'No canceled bookings found', + style: TextStyle(fontSize: 16), + ), + ); + } + + return ListView.builder( + itemCount: canceledBookings.length, + shrinkWrap: false, + itemBuilder: (context, index) { + final booking = canceledBookings[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Top Row: ID and "View order" + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + + const Divider(), + + /// Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (booking.images1 != null && + booking.images1!.isNotEmpty) + ? Image.network( + booking.images1!, + width: 100, + height: 110, + fit: BoxFit.cover, + errorBuilder: ( + context, + error, + stackTrace, + ) { + return Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ), + ), + + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.serviceName ?? 'No vendor name', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + booking.categoryName ?? + 'No service name', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 12.52 / 13.91, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + + const SizedBox(width: 10), + Container( + width: 70, + height: 25, + decoration: BoxDecoration( + color: const Color(0xFFFFEEEE), + borderRadius: BorderRadius.circular( + 6.05, + ), + ), + child: Center( + child: Text( + 'Cancel', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: const Color(0xFFFF0000), + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.lightGrey, + ), + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 15, + backgroundImage: + booking.profilePic1 != null && + booking + .profilePic1! + .isNotEmpty + ? NetworkImage( + booking.profilePic1!, + ) + : null, + child: + booking.profilePic1 == null || + booking + .profilePic1! + .isEmpty + ? const Icon( + Icons.person, + size: 20, + ) + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + booking.username ?? + 'No data', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w600, + fontSize: 11, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color( + 0xFF353434, + ), + ), + overflow: + TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 7), + Text( + 'client', + style: const TextStyle( + fontFamily: 'Gilroy-Regular', + fontWeight: FontWeight.w400, + fontSize: 11.84, + height: 10.65 / 11.84, + letterSpacing: 0, + color: Color(0xFF717171), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.cancelDate != null + ? _formatDate(booking.cancelDate!) + : 'No date', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.cancelDate != null + ? DateFormat('h:mm a').format( + DateTime.parse(booking.cancelDate!), + ) + : 'No time', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Working hours :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.workingHours ?? 'No hours', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + }, + ), + ), + ); + } + + String _formatDate(String dateString) { + try { + return DateFormat('dd-MM-yyyy').format(DateTime.parse(dateString)); + } catch (e) { + return dateString; // Fallback: return the original string if parsing fails + } + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_completed_booking.dart b/lib/view/vendor_main_screens/vendor_history/vendor_completed_booking.dart new file mode 100644 index 0000000..2d53ae2 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_completed_booking.dart @@ -0,0 +1,417 @@ + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; + +class VendorCompletedBooking extends ConsumerWidget { + // Changed to ConsumerWidget + const VendorCompletedBooking({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Added WidgetRef parameter + final bookingAsyncValue = ref.watch(vendorbookingdetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: bookingAsyncValue.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text('Error: $err')), + data: (bookings) { + // Filter only canceled bookings (status == 3) + final canceledBookings = + bookings.where((booking) => booking.status == 1).toList(); + + if (canceledBookings.isEmpty) { + return const Center( + child: Text( + 'No canceled bookings found', + style: TextStyle(fontSize: 16), + ), + ); + } + + return ListView.builder( + itemCount: canceledBookings.length, + shrinkWrap: false, + itemBuilder: (context, index) { + final booking = canceledBookings[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Top Row: ID and "View order" + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + + const Divider(), + + /// Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (booking.images1 != null && + booking.images1!.isNotEmpty) + ? Image.network( + booking.images1!, + width: 100, + height: 110, + fit: BoxFit.cover, + errorBuilder: ( + context, + error, + stackTrace, + ) { + return Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ), + ), + + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.serviceName ?? 'No vendor name', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + ), + + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + booking.categoryName ?? + 'No service name', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 12.52 / 13.91, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + ), + + const SizedBox(width: 10), + Container( + width: 70, + height: 25, + decoration: BoxDecoration( + color: const Color(0xffFFEEDB), + borderRadius: BorderRadius.circular( + 6.05, + ), + ), + child: Center( + child: Text( + 'Completed', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: const Color(0xffFF8515), + letterSpacing: 1.0, + height: 0.98, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? _formatDate( + booking.serviceDate!, + ) + : 'No date', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceTime.toString(), + + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Date / Time / Hours + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "Mobile number : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(width: 7), + Text( + booking.mobileNumber.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + children: [ + Text( + "E-mail ID :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(width: 7), + Text( + booking.email.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + children: [ + Text( + "Address : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(width: 7), + Text( + booking.address.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + SizedBox(height: 10), + Text( + "Message : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + SizedBox(height: 7), + Text( + booking.message.toString(), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + }, + ), + ), + ); + } + + String _formatDate(String dateString) { + try { + return DateFormat('dd-MM-yyyy').format(DateTime.parse(dateString)); + } catch (e) { + return dateString; // Fallback: return the original string if parsing fails + } + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_enquriy_list.dart b/lib/view/vendor_main_screens/vendor_history/vendor_enquriy_list.dart new file mode 100644 index 0000000..13d993d --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_enquriy_list.dart @@ -0,0 +1,395 @@ + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class VendorEnquriyList extends ConsumerStatefulWidget { + const VendorEnquriyList({super.key}); + + @override + ConsumerState createState() => _VendorEnquriyListState(); +} + +class _VendorEnquriyListState extends ConsumerState { + int? expandedIndex; + + @override + Widget build(BuildContext context) { + final enquiryListAsync = ref.watch(enquriylistProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: enquiryListAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + data: + (enquiries) => SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: List.generate(enquiries.length, (index) { + final enquiry = enquiries[index]; + final isExpanded = expandedIndex == index; + + return SizedBox( + width: double.infinity, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: AppColors.secondprimary, + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x66AEAEAE), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: + (enquiry.images1.isEmpty) + ? Container( + width: 89, + height: 64, + color: Colors.grey[300], + child: Icon( + Icons.image, + color: Colors.grey[600], + ), + ) + : Image.network( + enquiry.images1.first, + width: 89, + height: 64, + fit: BoxFit.cover, + errorBuilder: + ( + context, + error, + stackTrace, + ) => Container( + width: 89, + height: 64, + color: Colors.grey[300], + child: Icon( + Icons.broken_image, + color: Colors.grey[600], + ), + ), + ), + ), + + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + enquiry.vendorName ?? 'vendorName', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + height: 18.14 / 16, + letterSpacing: 0.16, + color: Colors.black, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + setState(() { + expandedIndex = + isExpanded ? null : index; + }); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: Text( + enquiry.serviceName, + textAlign: TextAlign.left, + maxLines: 2, + overflow: + TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w400, + fontSize: 16.11, + height: 17.3 / 16.11, + letterSpacing: 0.1611, + color: Colors.black, + ), + ), + ), + const SizedBox(width: 4), + Flex( + direction: Axis.horizontal, + mainAxisSize: + MainAxisSize.min, + children: [ + Text( + isExpanded + ? "View Less" + : "View More", + style: const TextStyle( + fontWeight: + FontWeight.w600, + fontSize: 13.15, + height: 5.71 / 13.15, + color: Color( + 0xFFFF3D00, + ), + ), + ), + const SizedBox(width: 5), + Icon( + isExpanded + ? Icons + .keyboard_arrow_up + : Icons + .keyboard_arrow_down, + color: const Color( + 0xffFF3D00, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + if (isExpanded) + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.43), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 0.82, + ), + boxShadow: const [ + BoxShadow( + color: Color(0xA9A9A940), + blurRadius: 3.29, + offset: Offset(0, 0.82), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text.rich( + TextSpan( + children: [ + const TextSpan( + text: "Name : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 13, + height: 1.239, + ), + ), + TextSpan( + text: enquiry.name ?? 'No Name', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.239, + color: Color(0xFF373636), + ), + ), + ], + ), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: AppColors.red, + size: 20, + ), + onPressed: () async { + final shouldDelete = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text( + 'Delete Enquiry', + ), + content: const Text( + 'Are you sure you want to delete this enquiry?', + ), + actions: [ + TextButton( + onPressed: + () => Navigator.of( + context, + ).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: + () => Navigator.of( + context, + ).pop(true), + child: const Text('Delete'), + ), + ], + ), + ); + + if (shouldDelete == true) { + Fluttertoast.showToast( + msg: 'Deleting enquiry...', + toastLength: + Toast + .LENGTH_SHORT, // ~2 seconds + gravity: ToastGravity.BOTTOM, + ); + + try { + final success = await ref.read( + enquriydeleteProvider( + enquiry.id.toString(), + ).future, + ); + + if (success) { + // Reset expanded index and refresh the list + setState(() { + expandedIndex = null; + }); + + await Future.delayed( + const Duration( + milliseconds: 300, + ), + ); + + ref.invalidate( + enquriylistProvider, + ); + + Fluttertoast.showToast( + msg: + 'Enquiry deleted successfully', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } else { + Fluttertoast.showToast( + msg: 'Failed to delete enquiry', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + } catch (e) { + Fluttertoast.showToast( + msg: 'Error: ${e.toString()}', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + } + }, + ), + ], + ), + const SizedBox(height: 8), + _infoText( + "Mobile number : ", + enquiry.mobile ?? 'No Mobile', + ), + const SizedBox(height: 13), + _infoText( + "E-mail Id : ", + enquiry.email ?? 'No Email', + ), + const SizedBox(height: 13), + _infoText( + "Message : ", + enquiry.message ?? 'No Message', + ), + ], + ), + ), + ], + ), + ); + }), + ), + ), + ), + ); + } + + Widget _infoText(String label, String value) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: label, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 13, + height: 1.239, + ), + ), + TextSpan( + text: value, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.239, + color: Color(0xFF373636), + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_history_maincontroller.dart b/lib/view/vendor_main_screens/vendor_history/vendor_history_maincontroller.dart new file mode 100644 index 0000000..430d843 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_history_maincontroller.dart @@ -0,0 +1,181 @@ +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/category_management.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_bookinglist.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_cancel_booking.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_completed_booking.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_enquriy_list.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_payment_details.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart'; +import 'package:flutter/material.dart'; + +class VendorHistoryMainController extends StatefulWidget { + final int? initialTabIndex; + + const VendorHistoryMainController({super.key, this.initialTabIndex}); + + @override + State createState() => + _VendorHistoryMainControllerState(); +} + +class _VendorHistoryMainControllerState + extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + int _currentIndex = 0; + TabController? _tabController; + + @override + bool get wantKeepAlive => true; + + final List tabTitles = [ + "Booking List", // Index 0 + "Category Management", // Index 1 + "Payment Details", // Index 2 + "Completed Booking", // Index 3 + "Enquiry List", // Index 4 + "Cancelation Booking", // Index 5 + ]; + + @override + void initState() { + super.initState(); + + // Set initial tab index from widget parameter + if (widget.initialTabIndex != null && + widget.initialTabIndex! >= 0 && + widget.initialTabIndex! < tabTitles.length) { + _currentIndex = widget.initialTabIndex!; + } + + // Initialize TabController with the correct initial index + _tabController = TabController( + length: tabTitles.length, + vsync: this, + initialIndex: _currentIndex, + ); + + _tabController!.addListener(() { + if (_tabController!.indexIsChanging) { + setState(() { + _currentIndex = _tabController!.index; + }); + } + }); + } + + @override + void dispose() { + _tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + if (_tabController == null) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return PopScope( + onPopInvoked: (didPop) { + if (didPop) { + // Reset main controller index to 0 when going back + final indexController = InheritedVendorIndexController.of(context); + indexController?.changeIndex(0); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(150.0), + child: Column( + children: [ + AppBar( + backgroundColor: Colors.white, + elevation: 0, + title: const Text( + "History", + style: TextStyle( + fontFamily: 'Inter', + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + centerTitle: true, + ), + Container( + margin: const EdgeInsets.only(top: 16.0), + alignment: Alignment.centerLeft, + height: 65, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TabBar( + controller: _tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + indicatorColor: Colors.transparent, + labelPadding: const EdgeInsets.symmetric(horizontal: 8.0), + tabs: List.generate( + tabTitles.length, + (index) => _buildCustomTab( + tabTitles[index], + index == _currentIndex, + ), + ), + onTap: (index) { + setState(() { + _currentIndex = index; + }); + }, + ), + ), + ), + ], + ), + ), + body: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + VendorBookinglist(), // Index 0 + CategoryManagement(), // Index 1 + VendorPaymentDetails(), // Index 2 + VendorCompletedBooking(), // Index 3 + VendorEnquriyList(), // Index 4 + VendorCancelBooking(), // Index 5 + ], + ), + ), + ); + } + + Widget _buildCustomTab(String text, bool isSelected) { + return Tab( + height: 53, + child: Container( + width: 180, + height: 45, + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : AppColors.secondprimary, + border: Border.all(color: const Color(0xFFDBDBDB)), + borderRadius: BorderRadius.circular(15), + ), + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 15, + height: 1.0, + letterSpacing: 0.5, + color: isSelected ? AppColors.secondprimary : AppColors.lightgray, + ), + ), + ), + ); + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_payment_details.dart b/lib/view/vendor_main_screens/vendor_history/vendor_payment_details.dart new file mode 100644 index 0000000..c7b653f --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_payment_details.dart @@ -0,0 +1,445 @@ + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:intl/intl.dart'; + +class VendorPaymentDetails extends ConsumerStatefulWidget { + const VendorPaymentDetails({super.key}); + + @override + ConsumerState createState() => + _VendorPaymentDetailsState(); +} + +class _VendorPaymentDetailsState extends ConsumerState { + List _localPayments = []; + + @override + Widget build(BuildContext context) { + final paymentAsync = ref.watch(paymentdetailsProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + body: paymentAsync.when( + data: (payments) { + final filteredPayments = payments.where((p) => p.type == 2).toList(); + + // Initialize local copy only once + if (_localPayments.isEmpty) { + _localPayments = List.from(filteredPayments); + } + + if (_localPayments.isEmpty) { + return const Center(child: Text("No payment history found.")); + } + + return RefreshIndicator( + onRefresh: () async { + final refreshed = await ref.refresh( + paymentdetailsProvider.future, + ); + setState(() { + _localPayments = refreshed.where((p) => p.type == 2).toList(); + }); + }, + child: ListView.builder( + itemCount: _localPayments.length, + itemBuilder: (context, index) { + final payment = _localPayments[index]; + return Dismissible( + key: Key(payment.id.toString()), + direction: DismissDirection.endToStart, + background: Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(20), + ), + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(Icons.delete, color: Colors.white), + ), + confirmDismiss: (direction) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Confirm Delete"), + content: const Text( + "Are you sure you want to delete this payment record?", + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("CANCEL"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text( + "DELETE", + style: TextStyle(color: Colors.red), + ), + ), + ], + ); + }, + ); + }, + onDismissed: (direction) async { + final scaffold = ScaffoldMessenger.of(context); + + Fluttertoast.showToast( + msg: 'Deleting payment...', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + final success = await ref.read( + paymentdeleteProvider(payment.id.toString()).future, + ); + + if (success) { + setState(() { + _localPayments.removeAt(index); + }); + + scaffold.showSnackBar( + SnackBar( + content: Text( + "${payment.planName} deleted successfully", + ), + ), + ); + } else { + scaffold.showSnackBar( + const SnackBar( + content: Text("Failed to delete payment"), + ), + ); + } + }, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFFCFCFCF), + width: 1, + ), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + width: 130, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF334E95), + borderRadius: BorderRadius.circular(12.86), + border: Border.all( + color: Colors.white, + width: 0.43, + ), + boxShadow: [ + BoxShadow( + color: const Color(0x40828282), + blurRadius: 1.71, + offset: const Offset(0, 2.14), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + payment.planName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.0, + letterSpacing: 0.1371, + color: Colors.white, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 15), + Row( + children: [ + Container( + width: 6.86, + height: 6.86, + decoration: const BoxDecoration( + color: Color(0xFFF9E369), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _formatDuration(payment.duration), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 12, + height: 1.0, + letterSpacing: 0.0771, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Container( + width: 6.86, + height: 6.86, + decoration: const BoxDecoration( + color: Color(0xFFF9E369), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + payment.description, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 12, + height: 1.0, + letterSpacing: 0.0771, + color: Colors.white, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const Divider(color: Colors.white), + Center( + child: Text( + _formatPrice(payment.price), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + payment.planName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 19.9, + height: 1.0, + letterSpacing: 0.199, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Start :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + DateFormat('MMM d, y').format( + DateTime.parse( + payment.createdDate, + ), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "End :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + DateFormat('MMM d, y').format( + DateTime.parse(payment.endDate), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Time :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + DateFormat('hh:mm a').format( + DateTime.parse( + payment.createdDate, + ), + ), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Payment :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15.61, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Text( + _formatPrice(payment.price), + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w700, + fontSize: 12, + height: 14.04 / 15.61, + letterSpacing: 0.0, + color: Color(0xFF636363), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text("Error: ${e.toString()}")), + ), + ); + } + + static String _formatDuration(int days) { + if (days == 30) return '1 Month'; + if (days == 60) return '2 Months'; + if (days == 90) return '3 Months'; + if (days == 180) return '6 Months'; + if (days == 365) return '1 Year'; + return '$days Days'; + } + + static String _formatPrice(String price) { + if (price.startsWith('Rs.')) { + return price; + } + return 'Rs.$price'; + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_service.dart b/lib/view/vendor_main_screens/vendor_history/vendor_service.dart new file mode 100644 index 0000000..b3cc185 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_service.dart @@ -0,0 +1,325 @@ +import 'dart:io'; + +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +class VendorService extends ConsumerStatefulWidget { + final int id; + final String tittle; + const VendorService({super.key, required this.id, required this.tittle}); + + @override + ConsumerState createState() => _VendorServiceState(); +} + +class _VendorServiceState extends ConsumerState { + @override + Widget build(BuildContext context) { + final vendorServiceAsyncValue = ref.watch(vendorserviceProvider); + + return Scaffold( + backgroundColor: AppColors.secondprimary, + appBar: AppBar( + backgroundColor: AppColors.secondprimary, + leading: const Icon(Icons.arrow_back_ios_new), + title: Text( + '${widget.tittle} Service List', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.0, + letterSpacing: 0.2, + ), + ), + centerTitle: true, + ), + body: vendorServiceAsyncValue.when( + data: (services) { + final filteredServices = services + .where((service) => service.category == widget.id) + .toList(); + + if (filteredServices.isEmpty) { + return const Center(child: Text("No services found")); + } + + return ListView.builder( + itemCount: filteredServices.length, + padding: const EdgeInsets.all(16), + itemBuilder: (context, index) { + final service = filteredServices[index]; + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFCFCFCF), width: 1), + boxShadow: [ + BoxShadow( + color: const Color(0x66A9A9A9), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox( + width: 100, + height: 100, + child: _buildServiceImage( + (service.images1.isNotEmpty + ? service.images1.first + : null), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + service.serviceName, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + height: 1.2, + letterSpacing: 0.2, + color: Colors.black, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Text( + 'Name: ${service.serviceName}', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF5A5A5A), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + 'Cat: ${service.categoryName}', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF5A5A5A), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + 'Sub-Cat: ${service.categoryName}', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF5A5A5A), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SizedBox( + height: 32, + child: ElevatedButton( + onPressed: () { + Get.toNamed( + RouterConts.vendorserviceupload, + arguments: service, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF0066FF, + ), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 20, + ), + side: const BorderSide( + color: Color(0xFFAAAAAA), + width: 0.5, + ), + ), + padding: EdgeInsets.zero, + ), + child: const FittedBox( + child: Text( + 'Edit', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.0, + letterSpacing: 0.28, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + Flexible( + child: SizedBox( + height: 32, + child: OutlinedButton( + onPressed: () async { + final result = await ref.read( + deleteserviceProvider( + service.id.toString(), + ).future, + ); + if (result) { + Fluttertoast.showToast( + msg: "Service deleted successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + ref.invalidate( + vendorserviceProvider, + ); // Refresh the service list + } else { + Fluttertoast.showToast( + msg: "Failed to delete service", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + }, + + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 20, + ), + ), + side: const BorderSide( + color: Color(0xFFAAAAAA), + width: 0.5, + ), + padding: EdgeInsets.zero, + ), + child: const FittedBox( + child: Text( + 'Delete', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.0, + letterSpacing: 0.28, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + "Error loading services: $err", + textAlign: TextAlign.center, + ), + ), + ), + ), + ); + } + + // Helper method to handle image loading with error handling + Widget _buildServiceImage(String? imagePath) { + if (imagePath == null || imagePath.isEmpty) { + return Container( + color: Colors.grey[200], + child: const Icon(Icons.work, size: 40, color: Colors.grey), + ); + } + + final bool isNetworkImage = imagePath.startsWith('http'); + final bool isValidFilePath = + imagePath.startsWith('/') || + (imagePath.startsWith('file://') && imagePath.length > 7); + + if (isNetworkImage) { + return CachedNetworkImage( + imageUrl: imagePath, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + color: Colors.grey[200], + child: const Center(child: CircularProgressIndicator()), + ), + errorWidget: (context, url, error) => Container( + color: Colors.grey[200], + child: const Icon(Icons.error, size: 40, color: Colors.red), + ), + ); + } else if (isValidFilePath) { + return Image.file( + File(imagePath.replaceFirst('file://', '')), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + print('Error loading image: $error'); + return Container( + color: Colors.grey[200], + child: const Icon(Icons.broken_image, size: 40, color: Colors.red), + ); + }, + ); + } else { + print('Invalid image path format: $imagePath'); + return Container( + color: Colors.grey[200], + child: const Icon( + Icons.image_not_supported, + size: 40, + color: Colors.grey, + ), + ); + } + } +} diff --git a/lib/view/vendor_main_screens/vendor_history/vendor_serviceupload.dart b/lib/view/vendor_main_screens/vendor_history/vendor_serviceupload.dart new file mode 100644 index 0000000..d4a8409 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_history/vendor_serviceupload.dart @@ -0,0 +1,1139 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/model/vendor_model/vendor_service_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_serviceupload_model.dart'; +import 'package:bookmywages/view/auth/auth_repository.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:bookmywages/viewmodel/consts_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; + +class VendorServiceupload extends ConsumerStatefulWidget { + final VendorServiceModel service; + const VendorServiceupload({super.key, required this.service}); + + @override + ConsumerState createState() => + _VendorServiceuploadState(); +} + +class _VendorServiceuploadState extends ConsumerState { + String? selectedCategory; + String? selectedSubCategory; + String? selectedWorkingHours; + String? selectedWorkingDuration; + String? selectedServiceType; + List selectedImages = []; + List existingImagePaths = []; // To track existing images + List videoLinks = []; + TextEditingController videoLinkController = TextEditingController(); + TextEditingController nameController = TextEditingController(); + TextEditingController serviceNameController = TextEditingController(); + TextEditingController serviceAmountController = TextEditingController(); + TextEditingController serviceDetailsController = TextEditingController(); + TextEditingController locationController = TextEditingController(); + TextEditingController descriptionController = TextEditingController(); + + final List workingDurationOptions = [ + '01:00 hours', + '02:00 hours', + '03:00 hours', + '04:00 hours', + '05:00 hours', + '06:00 hours', + ]; + + String? nameError; + String? serviceNameError; + String? categoryError; + String? subCategoryError; + String? workingHoursError; + String? workingDurationError; + String? serviceTypeError; + String? serviceAmountError; + String? serviceDetailsError; + String? locationError; + String? imagesError; + + List categories = []; + List subcategories = []; + bool isLoading = true; + bool isDataLoaded = false; + bool isSubmitting = false; + + @override + void initState() { + super.initState(); + _fetchCategories().then((_) { + _loadServiceData(); + }); + + // Add listeners to clear validation errors when typing + _setupTextFieldListeners(); + } + + void _setupTextFieldListeners() { + nameController.addListener(() { + if (nameError != null && nameController.text.isNotEmpty) { + setState(() => nameError = null); + } + }); + + serviceNameController.addListener(() { + if (serviceNameError != null && serviceNameController.text.isNotEmpty) { + setState(() => serviceNameError = null); + } + }); + + serviceAmountController.addListener(() { + if (serviceAmountError != null && + serviceAmountController.text.isNotEmpty) { + setState(() => serviceAmountError = null); + } + }); + + serviceDetailsController.addListener(() { + if (serviceDetailsError != null && + serviceDetailsController.text.isNotEmpty) { + setState(() => serviceDetailsError = null); + } + }); + + locationController.addListener(() { + if (locationError != null && locationController.text.isNotEmpty) { + setState(() => locationError = null); + } + }); + } + + // Helper method to safely construct image URLs + String _getImageUrl(String imagePath) { + String cleanPath = imagePath + .replaceAll('[', '') + .replaceAll(']', '') + .replaceAll('"', '') + .replaceAll("'", '') + .trim(); + + if (cleanPath.isEmpty) { + return ''; + } + + if (cleanPath.startsWith('http')) { + return cleanPath; + } + + return 'https://www.demo603.amrithaa.com/bookmywages/admin/public/images/$cleanPath'; + } + + String? _normalizeWorkingDuration(String? duration) { + if (duration == null || duration.isEmpty) return null; + + // Handle different formats that might come from the database + String normalizedDuration = duration.toLowerCase().trim(); + + // Extract number from the string + RegExp numberRegex = RegExp(r'(\d+)'); + Match? match = numberRegex.firstMatch(normalizedDuration); + + if (match != null) { + int hours = int.tryParse(match.group(1)!) ?? 0; + if (hours >= 1 && hours <= 6) { + return '${hours.toString().padLeft(2, '0')}:00 hours'; + } + } + + // If the format already matches, return as is + if (workingDurationOptions.contains(duration)) { + return duration; + } + + return null; // Return null if no match found + } + + void _loadServiceData() { + if (isDataLoaded) return; + + // Set the text field values + nameController.text = widget.service.vendorName; + serviceNameController.text = widget.service.serviceName; + serviceAmountController.text = widget.service.amount; + serviceDetailsController.text = widget.service.details; + locationController.text = widget.service.location; + descriptionController.text = widget.service.description; + + // Set dropdown values + selectedCategory = widget.service.category.toString(); + if (selectedCategory != null) { + _fetchSubcategories(selectedCategory!).then((_) { + setState(() { + selectedSubCategory = widget.service.subcategory.toString(); + }); + }); + } + + selectedWorkingHours = widget.service.workingHours; + selectedWorkingDuration = _normalizeWorkingDuration( + widget.service.workingDuration, + ); + selectedServiceType = widget.service.serviceType == 0 + ? 'Free Service' + : 'Paid Service'; + + // Handle existing images + _loadExistingImages(); + + // Handle existing videos + _loadExistingVideos(); + + setState(() { + isDataLoaded = true; + }); + } + + void _loadExistingImages() { + if (widget.service.images1.isNotEmpty) { + try { + setState(() { + existingImagePaths = List.from(widget.service.images1); + }); + } catch (e) { + print('Error loading images: $e'); + setState(() { + existingImagePaths = []; + }); + } + } + } + + void _loadExistingVideos() { + try { + setState(() { + videoLinks = List.from(widget.service.video!); + }); + } catch (e) { + print('Error loading videos: $e'); + setState(() { + videoLinks = []; + }); + } + } + + @override + void dispose() { + nameController.dispose(); + serviceNameController.dispose(); + serviceAmountController.dispose(); + serviceDetailsController.dispose(); + locationController.dispose(); + descriptionController.dispose(); + videoLinkController.dispose(); + super.dispose(); + } + + Future _fetchCategories() async { + try { + final categoryData = await ref.read(categoryListProvider.future); + setState(() { + categories = categoryData; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + print('Error fetching categories: $e'); + } + } + + Future _fetchSubcategories(String categoryId) async { + setState(() { + isLoading = true; + subcategories = []; + if (!isDataLoaded) { + selectedSubCategory = null; + } + subCategoryError = null; + }); + + try { + final subcategoryData = await ref.read( + subcategoryProvider(categoryId).future, + ); + setState(() { + subcategories = subcategoryData; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + print('Error fetching subcategories: $e'); + } + } + + Future _pickImages() async { + final picker = ImagePicker(); + final pickedFiles = await picker.pickMultiImage(); + if (pickedFiles.isNotEmpty) { + setState(() { + selectedImages.addAll(pickedFiles.map((file) => File(file.path))); + imagesError = null; + }); + } + } + + void _addVideoLink() { + if (videoLinkController.text.trim().isNotEmpty) { + setState(() { + videoLinks.add(videoLinkController.text.trim()); + videoLinkController.clear(); + }); + } + } + + void _removeImage(int index) { + setState(() { + selectedImages.removeAt(index); + if (selectedImages.isEmpty && existingImagePaths.isEmpty) { + imagesError = 'Please upload at least one image'; + } + }); + } + + void _removeExistingImage(int index) { + setState(() { + existingImagePaths.removeAt(index); + if (existingImagePaths.isEmpty && selectedImages.isEmpty) { + imagesError = 'Please upload at least one image'; + } + }); + } + + void _removeVideoLink(int index) { + setState(() { + videoLinks.removeAt(index); + }); + } + + bool _validateForm() { + bool isValid = true; + + if (nameController.text.isEmpty) { + setState(() => nameError = 'Please enter your name'); + isValid = false; + } + + if (serviceNameController.text.isEmpty) { + setState(() => serviceNameError = 'Please enter service name'); + isValid = false; + } + + if (selectedCategory == null) { + setState(() => categoryError = 'Please select a category'); + isValid = false; + } + + if (selectedSubCategory == null) { + setState(() => subCategoryError = 'Please select a sub-category'); + isValid = false; + } + + if (selectedWorkingHours == null) { + setState(() => workingHoursError = 'Please select working hours'); + isValid = false; + } + + if (selectedWorkingDuration == null) { + setState(() => workingDurationError = 'Please select working duration'); + isValid = false; + } + + if (selectedServiceType == null) { + setState(() => serviceTypeError = 'Please select service type'); + isValid = false; + } + + // if (serviceAmountController.text.isEmpty) { + // setState(() => serviceAmountError = 'Please enter service amount'); + // isValid = false; + // } + + if (serviceDetailsController.text.isEmpty) { + setState(() => serviceDetailsError = 'Please enter service details'); + isValid = false; + } + + if (locationController.text.isEmpty) { + setState(() => locationError = 'Please enter location'); + isValid = false; + } + + if (selectedImages.isEmpty && existingImagePaths.isEmpty) { + setState(() => imagesError = 'Please upload at least one image'); + isValid = false; + } + + return isValid; + } + + List generateWorkingHours() { + List workingHours = []; + TimeOfDay startTime = TimeOfDay(hour: 9, minute: 0); + TimeOfDay endTime = TimeOfDay(hour: 18, minute: 0); + + while (startTime.hour < endTime.hour || + (startTime.hour == endTime.hour && + startTime.minute <= endTime.minute)) { + final hour = startTime.hourOfPeriod.toString().padLeft(2, '0'); + final minute = startTime.minute.toString().padLeft(2, '0'); + final period = startTime.period == DayPeriod.am ? 'am' : 'pm'; + workingHours.add('$hour:$minute $period'); + + int newMinute = startTime.minute + 30; + int newHour = startTime.hour; + if (newMinute >= 60) { + newMinute -= 60; + newHour += 1; + } + startTime = TimeOfDay(hour: newHour, minute: newMinute); + } + + return workingHours; + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final keyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0; + + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: false, + body: SingleChildScrollView( + padding: EdgeInsets.only(bottom: keyboardOpen ? 20 : 0), + child: Column( + children: [ + Image.asset( + AppAssets.login, + width: screenSize.width, + height: screenSize.height * 0.45, + fit: BoxFit.cover, + ), + Transform.translate( + offset: const Offset(0, -100), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: screenSize.width * 0.05, + ), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.73), + border: Border.all( + width: 1.09, + color: const Color(0xFFA39898), + ), + boxShadow: const [ + BoxShadow( + offset: Offset(0, -4), + blurRadius: 12.3, + color: Color(0x66E0E0E0), + ), + BoxShadow( + offset: Offset(0, 4), + blurRadius: 4, + color: Color(0x66000000), + ), + ], + color: Colors.white, + ), + child: Padding( + padding: EdgeInsets.all(screenSize.width * 0.05), + child: Column( + children: [ + const Text( + "Edit Service", + style: TextStyle( + fontSize: 25, + fontFamily: "Gilroy-ExtraBold", + fontWeight: FontWeight.w800, + height: 1.0, + letterSpacing: 0.64, + ), + ), + const SizedBox(height: 25), + CommonTextFormField( + controller: nameController, + hintText: 'Enter your name', + errorText: nameError, + prefixIcon: const Icon( + Icons.person, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + CommonTextFormField( + controller: serviceNameController, + hintText: 'Service name', + errorText: serviceNameError, + prefixIcon: const Icon( + Icons.build, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Categories', + Icons.category, + errorText: categoryError, + ), + value: selectedCategory, + items: _buildCategoryItems(), + onChanged: (value) { + setState(() { + selectedCategory = value; + selectedSubCategory = null; + categoryError = null; + }); + if (value != null) _fetchSubcategories(value); + }, + isExpanded: true, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Sub-categories', + Icons.list, + errorText: subCategoryError, + ), + value: selectedSubCategory, + items: _buildSubcategoryItems(), + onChanged: subcategories.isNotEmpty + ? (value) { + setState(() { + selectedSubCategory = value; + subCategoryError = null; + }); + } + : null, + isExpanded: true, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Working Hours', + Icons.access_time, + errorText: workingHoursError, + ), + value: selectedWorkingHours, + items: generateWorkingHours() + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + selectedWorkingHours = value; + workingHoursError = null; + }); + }, + isExpanded: true, + menuMaxHeight: 300, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Select Working Duration', + Icons.timelapse, + errorText: workingDurationError, + ), + value: selectedWorkingDuration, + items: workingDurationOptions + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + selectedWorkingDuration = value; + workingDurationError = null; + }); + }, + isExpanded: true, + ), + const SizedBox(height: 20), + DropdownButtonFormField( + decoration: _buildDropdownDecoration( + 'Service Free or Paid', + Icons.money, + errorText: serviceTypeError, + ), + value: selectedServiceType, + items: ['Free Service', 'Paid Service'] + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + selectedServiceType = value; + serviceTypeError = null; + // Clear the amount when switching to free service + if (value == 'Free Service') { + serviceAmountController.clear(); + serviceAmountError = null; + } + }); + }, + isExpanded: true, + ), + const SizedBox(height: 20), + // Conditionally show the service amount field + if (selectedServiceType == 'Paid Service') ...[ + CommonTextFormField( + controller: serviceAmountController, + hintText: 'Service Amount', + errorText: serviceAmountError, + prefixIcon: const Icon( + Icons.currency_rupee, + color: AppColors.hittext, + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 20), + ], + + CommonTextFormField( + controller: serviceDetailsController, + hintText: 'Service Details', + errorText: serviceDetailsError, + prefixIcon: const Icon( + Icons.description, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + CommonTextFormField( + controller: locationController, + hintText: 'Location', + errorText: locationError, + prefixIcon: const Icon( + Icons.location_on, + color: AppColors.hittext, + ), + ), + const SizedBox(height: 20), + CommonTextFormField( + controller: descriptionController, + hintText: 'Description (Optional)', + prefixIcon: Padding( + padding: const EdgeInsets.only(bottom: 48), + child: Icon( + Icons.description, + color: AppColors.hittext, + ), + ), + maxLines: 5, + minLines: 3, + keyboardType: TextInputType.multiline, + ), + const SizedBox(height: 20), + // Image Upload Section + _buildImageUploadSection(), + const SizedBox(height: 20), + // Video Links Section + _buildVideoLinksSection(), + const SizedBox(height: 30), + _buildSubmitButton(), + SizedBox(height: 30), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildImageUploadSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: _pickImages, + child: Container( + width: 349, + height: 56.12, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.78), + border: Border.all(color: const Color(0xFFBDBCBC), width: 0.88), + ), + child: const Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.image, color: AppColors.hittext), + SizedBox(width: 10), + Text( + 'Upload Images', + style: TextStyle(color: AppColors.hittext), + ), + ], + ), + ), + ), + ), + if (imagesError != null) + Padding( + padding: const EdgeInsets.only(left: 16, top: 8), + child: Text( + imagesError!, + style: TextStyle(color: Colors.red[700], fontSize: 12), + ), + ), + // Display existing images + if (existingImagePaths.isNotEmpty) ...[ + const SizedBox(height: 10), + _buildExistingImagesSection(), + ], + // Display newly selected images + if (selectedImages.isNotEmpty) ...[ + const SizedBox(height: 10), + _buildNewImagesSection(), + ], + ], + ); + } + + Widget _buildExistingImagesSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0), + child: Text( + "Existing Images:", + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: existingImagePaths.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Stack( + children: [ + Container( + width: 90, + height: 90, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.grey[200], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + _getImageUrl(existingImagePaths[index]), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[300], + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.broken_image, + color: Colors.grey[600], + size: 24, + ), + SizedBox(height: 4), + Text( + 'Image not found', + style: TextStyle( + fontSize: 8, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: + loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => _removeExistingImage(index), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + width: 20, + height: 18, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.8), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.white, + size: 13, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildNewImagesSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0), + child: Text( + "New Images:", + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: selectedImages.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(right: 10), + child: Stack( + children: [ + Container( + width: 90, + height: 90, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + image: DecorationImage( + image: FileImage(selectedImages[index]), + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => _removeImage(index), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + width: 20, + height: 18, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.8), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.white, + size: 13, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildVideoLinksSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: CommonTextFormField( + controller: videoLinkController, + hintText: 'Add YouTube video link (Optional)', + prefixIcon: const Icon( + Icons.video_library, + color: AppColors.hittext, + ), + ), + ), + IconButton( + icon: const Icon(Icons.add, color: AppColors.primary), + onPressed: _addVideoLink, + ), + ], + ), + const SizedBox(height: 4), + const Text( + 'Optional - Add videos of your service', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + // Display added video links + if (videoLinks.isNotEmpty) ...[ + const SizedBox(height: 10), + Column( + children: videoLinks.map((link) { + return ListTile( + leading: const Icon( + Icons.video_library, + color: AppColors.primary, + ), + title: Text( + link, + maxLines: 1, + style: const TextStyle(fontSize: 13), + ), + trailing: IconButton( + icon: const Icon(Icons.close, size: 23, color: Colors.red), + onPressed: () => _removeVideoLink(videoLinks.indexOf(link)), + ), + ); + }).toList(), + ), + ], + ], + ); + } + + Widget _buildSubmitButton() { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: isSubmitting ? null : _handleSubmit, + child: isSubmitting + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + 'Submit', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + Future _handleSubmit() async { + if (_validateForm()) { + setState(() { + isSubmitting = true; + }); + + try { + final model = VendorServiceUploadModel( + id: widget.service.id.toString(), + vendorName: nameController.text, + serviceName: serviceNameController.text, + serviceType: selectedServiceType == 'Free Service' ? '0' : '1', + + category: selectedCategory!, + subcategory: selectedSubCategory!, + workingHours: selectedWorkingHours!, + workingDuration: selectedWorkingDuration!, + amount: double.tryParse(serviceAmountController.text) ?? 0.0, + location: locationController.text, + description: descriptionController.text, + details: serviceDetailsController.text, + images: existingImagePaths, + videos: videoLinks, + ); + + // Use the updated service class + final serviceUpdate = Vendorserviceupadate(); + final result = await serviceUpdate.vendorserviceupdate( + ConstsApi + .vendorserviceupload, // Replace with your actual API endpoint + model, + selectedImages, + ); + + Fluttertoast.showToast( + msg: 'Service updated successfully!', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + ); + ref.invalidate(vendorserviceProvider); + Navigator.pop(context, true); // Return true to indicate success + } catch (e) { + Fluttertoast.showToast( + msg: 'Error updating service: $e', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } finally { + setState(() { + isSubmitting = false; + }); + } + } + } + + List> _buildCategoryItems() { + if (categories.isEmpty) { + return [ + const DropdownMenuItem( + value: null, + child: Text('No categories available'), + ), + ]; + } + return categories.map>((cat) { + try { + final id = cat.id?.toString() ?? ''; + final name = cat.name?.toString() ?? 'Unknown'; + return DropdownMenuItem(value: id, child: Text(name)); + } catch (e) { + print('Error: $e'); + return const DropdownMenuItem( + value: null, + child: Text('Error'), + ); + } + }).toList(); + } + + List> _buildSubcategoryItems() { + if (subcategories.isEmpty) { + return [ + DropdownMenuItem( + value: null, + child: Text( + selectedCategory == null + ? 'Select a category first' + : isLoading + ? 'Loading subcategories...' + : 'No subcategories available', + ), + ), + ]; + } + return subcategories.map>((sub) { + try { + final id = sub.id?.toString() ?? ''; + final name = sub.name?.toString() ?? 'Unknown'; + return DropdownMenuItem(value: id, child: Text(name)); + } catch (e) { + print('Error: $e'); + return const DropdownMenuItem( + value: null, + child: Text('Error'), + ); + } + }).toList(); + } + + InputDecoration _buildDropdownDecoration( + String hintText, + IconData icon, { + String? errorText, + }) { + return InputDecoration( + filled: true, + fillColor: Colors.white, + prefixIcon: Icon(icon, color: AppColors.hittext), + hintText: hintText, + hintStyle: const TextStyle(color: AppColors.hittext), + errorText: errorText, + errorStyle: TextStyle(color: Colors.red[700], fontSize: 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: const BorderSide(color: Color(0xFFBDBCBC), width: 0.88), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: const BorderSide(color: Color(0xFFBDBCBC), width: 0.88), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: BorderSide(color: Colors.red[700]!, width: 0.88), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.78), + borderSide: BorderSide(color: Colors.red[700]!, width: 0.88), + ), + ); + } +} diff --git a/lib/view/vendor_main_screens/vendor_homepage.dart b/lib/view/vendor_main_screens/vendor_homepage.dart new file mode 100644 index 0000000..5fe4899 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_homepage.dart @@ -0,0 +1,1998 @@ +import 'dart:ui'; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/vendor_flow_drawer.dart'; +import 'package:bookmywages/model/cancel_booking.dart'; +import 'package:bookmywages/model/vendor_model/vendor_booking_status.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/routers/router.dart'; +import 'package:bookmywages/view/user_main_screens/main_contoller.dart'; +import 'package:bookmywages/view/user_main_screens/notification_page.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; // For date formatting + +class VendorHomepage extends ConsumerStatefulWidget { + const VendorHomepage({super.key}); + + @override + ConsumerState createState() => _VendorHomepageState(); +} + +class _VendorHomepageState extends ConsumerState + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + final GlobalKey _scaffoldKey = GlobalKey(); + final CarouselSliderController _controller = CarouselSliderController(); + + int? expandedIndex; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + Future.microtask(() async { + final prefs = await SharedPreferences.getInstance(); + final vendorId = prefs.getString('vendor_id') ?? ''; + + await updateNotificationCount(ref, type: 2, userId: vendorId); + }); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed && mounted) { + _refreshData(); + } + } + + void _refreshData() { + if (!mounted) return; + try { + ref.invalidate(bannerListProvider); + ref.invalidate(vendorbookingdetailsProvider); + ref.invalidate(vendorserviceProvider); + ref.invalidate(enquriylistProvider); + ref.invalidate(vendorexpiredPlanProvider); + } catch (e) { + // Handle refresh error silently + } + } + + // Helper function to get status color and text + Map getStatusInfo(int status) { + switch (status) { + case 0: + return { + 'text': 'Pending', + 'color': Colors.orange, + 'bgColor': Colors.orange.shade100, + }; + case 1: + return { + 'text': 'Scheduled', + 'color': Colors.green, + 'bgColor': Colors.green.shade100, + }; + case 2: + return { + 'text': 'Completed', + 'color': Colors.blue, + 'bgColor': Colors.blue.shade100, + }; + case 3: + return { + 'text': 'Canceled', + 'color': Colors.red, + 'bgColor': Colors.red.shade100, + }; + default: + return { + 'text': 'pending', + 'color': Colors.blue, + 'bgColor': Colors.blue.shade100, + }; + } + } + + // Format date from API response + String formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + return DateFormat('MMMM dd, yyyy').format(date); + } catch (e) { + return dateStr; + } + } + + String _formatDate(String dateString) { + try { + return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString)); + } catch (e) { + return dateString; + } + } + + Widget _infoText(String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 13, + height: 1.239, + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.239, + color: Color(0xFF373636), + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ); + } + + Widget _buildBannerSection(AsyncValue bannerAsyncValue) { + return bannerAsyncValue.when( + data: (banners) { + if (banners == null || banners.isEmpty) { + return Container( + height: 180, + margin: const EdgeInsets.only(left: 16, top: 40, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.grey[200], + ), + child: const Center( + child: Icon(Icons.image, color: Colors.grey, size: 50), + ), + ); + } + + final imageUrls = banners.map((b) => b.documentUrl).toList(); + + return Padding( + padding: const EdgeInsets.only(left: 16, top: 40, right: 16), + child: CarouselSlider( + options: CarouselOptions( + height: 180, + autoPlay: true, + enlargeCenterPage: true, + viewportFraction: 1.0, + autoPlayCurve: Curves.fastOutSlowIn, + enableInfiniteScroll: true, + ), + carouselController: _controller, + items: imageUrls.map((url) { + return Builder( + builder: (BuildContext context) { + return Container( + height: 180, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + image: DecorationImage( + image: NetworkImage(url), + fit: BoxFit.cover, + onError: (exception, stackTrace) { + // Handle image loading error silently + }, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Left arrow + Transform.translate( + offset: const Offset(-15, 0), + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: GestureDetector( + onTap: () { + try { + _controller.previousPage(); + } catch (e) { + // Handle carousel error silently + } + }, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Image.asset( + AppAssets.arrowbutton, + width: 50, + height: 50, + color: AppColors.thridprimary, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.arrow_back, + size: 20, + ); + }, + ), + ), + ), + ), + ), + ), + // Right arrow + Transform.translate( + offset: const Offset(15, 0), + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + onTap: () { + try { + _controller.nextPage(); + } catch (e) { + // Handle carousel error silently + } + }, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Transform.rotate( + angle: 3.14, + child: Image.asset( + AppAssets.arrowbutton, + width: 50, + height: 50, + color: AppColors.thridprimary, + errorBuilder: + (context, error, stackTrace) { + return const Icon( + Icons.arrow_forward, + size: 20, + ); + }, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }).toList(), + ), + ); + }, + loading: () => Container( + height: 180, + margin: const EdgeInsets.only(left: 16, top: 40, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.grey[200], + ), + child: const Center( + child: Icon(Icons.image, color: Colors.grey, size: 50), + ), + ), + error: (err, stack) => Container( + height: 180, + margin: const EdgeInsets.only(left: 16, top: 40, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.grey[200], + ), + child: const Center( + child: Icon(Icons.image, color: Colors.grey, size: 50), + ), + ), + ); + } + + Widget _buildExpiredPlanSection(AsyncValue expiredPlan) { + return expiredPlan.when( + data: (plan) { + if (plan == null || plan.endDate == null || plan.planName == null) { + return const SizedBox(); + } + + String formattedEndDate = ''; + try { + final date = DateTime.parse(plan.endDate.toString()); + formattedEndDate = DateFormat('MMMM dd').format(date); + } catch (_) { + formattedEndDate = plan.endDate?.toString() ?? 'Not available'; + } + + final indexController = InheritedVendorIndexController.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: AppColors.lightBlue, + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Subscription plan', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 24, + height: 27.59 / 24, + letterSpacing: 1.0, + color: Color(0xFF9C34C2), + ), + ), + GestureDetector( + onTap: () async { + try { + // Get the index controller reference + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 2, // Enquiry list tab + }, + ); + } catch (e) { + // Handle navigation error silently + debugPrint('Navigation error: $e'); + } + }, + child: Text( + 'View more', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 15, + height: 11.17 / 15, + letterSpacing: 0.0, + color: Color(0xFF534E4E), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Image.asset( + AppAssets.subscription, + width: 60, + height: 60, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 60, + height: 60, + color: Colors.grey[200], + child: const Icon(Icons.subscriptions), + ); + }, + ), + const SizedBox(width: 16), + Flexible( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Your ', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFF585454), + fontWeight: FontWeight.w400, + fontSize: 18, + height: 1.86, + letterSpacing: 0.1957, + ), + ), + TextSpan( + text: plan.planName ?? 'Subscription', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFFFF0000), + fontWeight: FontWeight.w700, + fontSize: 18, + height: 1.86, + letterSpacing: 0.1957, + ), + ), + TextSpan( + text: ' subscription plan was expired on ', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFF585454), + fontWeight: FontWeight.w400, + fontSize: 18, + height: 1.86, + letterSpacing: 0.1957, + ), + ), + TextSpan( + text: formattedEndDate, + style: TextStyle( + fontFamily: 'Gilroy-Medium', + color: Color(0xFFFF0000), + fontWeight: FontWeight.w700, + fontSize: 18, + letterSpacing: 0.1957, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + 'Start Date: ${plan.createdDate?.split(' ').first ?? 'Not available'}', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14, + height: 13 / 14, + letterSpacing: 0.14, + color: Color(0xFF4F4F4F), + ), + ), + ), + Flexible( + child: Text( + 'End Date: ${plan.endDate ?? 'Not available'}', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14, + height: 13 / 14, + letterSpacing: 0.14, + color: Color(0xFF4F4F4F), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () { + try { + Get.offAllNamed( + RouterConts.vendorpackage, + arguments: 1, + ); + } catch (e) { + // Handle navigation error silently + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + child: const Text( + 'Renewal', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + loading: () => const SizedBox(), + error: (e, _) => const SizedBox(), + ); + } + + Widget _buildBookingsSection(AsyncValue bookingsAsyncValue) { + return bookingsAsyncValue.when( + data: (bookings) { + if (bookings == null || bookings.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + // Bookings header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Your Bookings', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + GestureDetector( + onTap: () async { + try { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 0, // Enquiry list tab + }, + ); + } catch (e) { + // Handle navigation error silently + debugPrint('Navigation error: $e'); + } + }, + child: const Text( + 'View more', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.thridprimary, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Bookings list + ListView.builder( + itemCount: bookings.length, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemBuilder: (context, index) { + final booking = bookings[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.lightGrey), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ID and View order + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + top: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'ID : ${booking.id ?? 'N/A'}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + const Divider(), + + // Image, Company, and Status + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (booking.images1 != null && + booking.images1!.isNotEmpty) + ? Image.network( + booking.images1!, + width: 100, + height: 110, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) { + return Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + AppAssets.cleaning, + width: 100, + height: 110, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + const Text( + 'Live', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + booking.name ?? 'No data', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16.11, + height: 14.5 / 16.11, + letterSpacing: 0.01 * 16.11, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + flex: 3, + child: Text( + booking.serviceName ?? '', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w500, + fontSize: 13.91, + height: 1.3, + letterSpacing: 0.01 * 13.91, + color: Color(0xFF5A5A5A), + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 2, + child: Container( + height: 25, + decoration: BoxDecoration( + color: booking.status == 3 + ? const Color(0xFFFFEEEE) + : booking.status == 1 + ? const Color(0xFFDAE9FF) + : const Color(0xFFE6F7E6), + borderRadius: + BorderRadius.circular(6.05), + ), + child: Center( + child: Text( + booking.status == 3 + ? 'Cancel' + : booking.status == 1 + ? 'completed' + : booking.status == 4 + ? 'Scheduled' + : 'pending', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontSize: 10.92, + fontWeight: FontWeight.w400, + color: booking.status == 3 + ? const Color(0xFFFF0000) + : booking.status == 1 + ? const Color(0xFF0066FF) + : const Color(0xFF2E8B57), + letterSpacing: 1.0, + height: 0.98, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Date :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? _formatDate( + booking.serviceDate!, + ) + : 'No date', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Time :', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + booking.serviceDate != null + ? DateFormat( + 'h:mm a', + ).format( + DateTime.parse( + booking.serviceDate!, + ), + ) + : 'No time', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + /// Contact information + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + "Mobile number : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(width: 7), + Expanded( + child: Text( + booking.mobileNumber?.toString() ?? 'N/A', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 13), + Row( + children: [ + const Text( + "E-mail ID :", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(width: 7), + Expanded( + child: Text( + booking.email?.toString() ?? 'N/A', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 13), + Row( + children: [ + const Text( + "Address : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(width: 7), + Expanded( + child: Text( + booking.address?.toString() ?? 'N/A', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ), + const SizedBox(height: 13), + const Text( + "Message : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + ), + const SizedBox(height: 13), + Text( + booking.message?.toString() ?? 'N/A', + style: const TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(height: 20), + + // Action buttons based on booking status + if (booking.status == 0) + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + try { + final data = VendorBookingStatus( + id: booking.id.toString(), + status: "4", + ); + + await ref.read( + changebookingProvider(data).future, + ); + + if (mounted) { + Fluttertoast.showToast( + msg: + 'Booking confirmed successfully', + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + _refreshData(); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: 'Failed to confirm booking', + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text( + "Confirmed", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w800, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.289, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () async { + try { + final data = CancelBookingRequest( + id: booking.id.toString(), + serviceId: + booking.serviceId?.toString() ?? + "0", + type: booking.type?.toString() ?? "0", + ); + + await ref.read( + cancelbookingProvider(data).future, + ); + + if (mounted) { + Fluttertoast.showToast( + msg: + 'Booking cancelled successfully', + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + _refreshData(); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: 'Failed to cancel booking', + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + } + }, + child: Container( + height: 42, + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xff534E4E), + ), + borderRadius: BorderRadius.circular(20), + ), + child: const Center( + child: Text( + 'Cancel Booking', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.02, + color: Color(0xff534E4E), + ), + ), + ), + ), + ), + ), + ], + ), + ) + else if (booking.status == 4) + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: null, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text( + "pending", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w800, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.289, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () async { + try { + final data = VendorBookingStatus( + id: booking.id.toString(), + status: "1", + ); + + await ref.read( + changebookingProvider(data).future, + ); + + if (mounted) { + Fluttertoast.showToast( + msg: + 'Booking completed successfully', + backgroundColor: Colors.green, + textColor: Colors.white, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + _refreshData(); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: 'Failed to complete booking', + backgroundColor: Colors.red, + textColor: Colors.white, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + ); + } + } + }, + child: Container( + height: 42, + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xff534E4E), + ), + borderRadius: BorderRadius.circular(20), + ), + child: const Center( + child: Text( + 'complete', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: 14.45, + height: 13 / 14.45, + letterSpacing: 0.02, + color: Color(0xff534E4E), + ), + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ); + }, + ), + ], + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (err, stack) => const SizedBox.shrink(), + ); + } + + Widget _buildEnquirySection(AsyncValue enquiryListAsync) { + return enquiryListAsync.when( + data: (enquiries) { + if (enquiries == null || enquiries.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + // User Enquiry Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'User Enquiry', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + GestureDetector( + onTap: () async { + try { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 4, // Enquiry list tab + }, + ); + } catch (e) { + // Handle navigation error silently + debugPrint('Navigation error: $e'); + } + }, + child: const Text( + 'View more', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.thridprimary, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Enquiry List + ListView.builder( + itemCount: enquiries.length, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemBuilder: (context, index) { + final enquiry = enquiries[index]; + final isExpanded = expandedIndex == index; + + return SizedBox( + width: double.infinity, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: AppColors.secondprimary, + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x66AEAEAE), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: (enquiry.images1?.isEmpty ?? true) + ? Container( + width: 89, + height: 64, + color: Colors.grey[300], + child: Icon( + Icons.image, + color: Colors.grey[600], + ), + ) + : Image.network( + enquiry.images1!.first, + width: 89, + height: 64, + fit: BoxFit.cover, + errorBuilder: + ( + context, + error, + stackTrace, + ) => Container( + width: 89, + height: 64, + color: Colors.grey[300], + child: Icon( + Icons.broken_image, + color: Colors.grey[600], + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + enquiry.vendorName ?? 'vendorName', + style: const TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 16, + height: 18.14 / 16, + letterSpacing: 0.16, + color: Colors.black, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + if (mounted) { + setState(() { + expandedIndex = isExpanded + ? null + : index; + }); + } + }, + child: Row( + children: [ + Expanded( + child: Text( + enquiry.serviceName ?? '', + textAlign: TextAlign.left, + maxLines: 2, + overflow: + TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w400, + fontSize: 16.11, + height: 17.3 / 16.11, + letterSpacing: 0.1611, + color: Colors.black, + ), + ), + ), + const SizedBox(width: 4), + Row( + mainAxisSize: + MainAxisSize.min, + children: [ + Text( + isExpanded + ? "View Less" + : "View More", + style: const TextStyle( + fontWeight: + FontWeight.w600, + fontSize: 13.15, + height: 5.71 / 13.15, + color: Color( + 0xFFFF3D00, + ), + ), + ), + const SizedBox(width: 5), + Icon( + isExpanded + ? Icons + .keyboard_arrow_up + : Icons + .keyboard_arrow_down, + color: const Color( + 0xffFF3D00, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + + // Animated expansion + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: isExpanded ? null : 0, + child: isExpanded + ? Container( + width: double.infinity, + margin: const EdgeInsets.symmetric( + horizontal: 16, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.43), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 0.82, + ), + boxShadow: const [ + BoxShadow( + color: Color(0xA9A9A940), + blurRadius: 3.29, + offset: Offset(0, 0.82), + ), + ], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: "Name : ", + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: + FontWeight.w700, + fontSize: 13, + height: 1.239, + ), + ), + TextSpan( + text: + enquiry.name ?? + 'No Name', + style: const TextStyle( + fontFamily: + 'Gilroy-Medium', + fontWeight: + FontWeight.w400, + fontSize: 13, + height: 1.239, + color: Color(0xFF373636), + ), + ), + ], + ), + ), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: AppColors.red, + size: 20, + ), + onPressed: () async { + final shouldDelete = + await showDialog( + context: context, + builder: (context) => + AlertDialog( + title: const Text( + 'Delete Enquiry', + ), + content: const Text( + 'Are you sure you want to delete this enquiry?', + ), + actions: [ + TextButton( + onPressed: () => + Navigator.of( + context, + ).pop(false), + child: const Text( + 'Cancel', + ), + ), + TextButton( + onPressed: () => + Navigator.of( + context, + ).pop(true), + child: const Text( + 'Delete', + ), + ), + ], + ), + ); + + if (shouldDelete == true && + mounted) { + try { + final success = await ref + .read( + enquriydeleteProvider( + enquiry.id.toString(), + ).future, + ); + + if (success && mounted) { + setState(() { + expandedIndex = null; + }); + + await Future.delayed( + const Duration( + milliseconds: 300, + ), + ); + + _refreshData(); + + Fluttertoast.showToast( + msg: + 'Enquiry deleted successfully', + toastLength: + Toast.LENGTH_SHORT, + gravity: + ToastGravity.BOTTOM, + ); + } else if (mounted) { + Fluttertoast.showToast( + msg: + 'Failed to delete enquiry', + toastLength: + Toast.LENGTH_SHORT, + gravity: + ToastGravity.BOTTOM, + ); + } + } catch (e) { + if (mounted) { + Fluttertoast.showToast( + msg: + 'Error deleting enquiry', + toastLength: + Toast.LENGTH_SHORT, + gravity: + ToastGravity.BOTTOM, + ); + } + } + } + }, + ), + ], + ), + const SizedBox(height: 8), + _infoText( + "Mobile number : ", + enquiry.mobile ?? '', + ), + const SizedBox(height: 13), + _infoText( + "E-mail Id : ", + enquiry.email ?? '', + ), + const SizedBox(height: 13), + _infoText( + "Message : ", + enquiry.message ?? '', + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + ); + }, + ), + ], + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (error, stack) => const SizedBox.shrink(), + ); + } + + Widget _buildServicesSection(AsyncValue vendorServiceAsyncValue) { + return vendorServiceAsyncValue.when( + data: (services) { + if (services == null || services.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Your Service', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + GestureDetector( + onTap: () async { + try { + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 1, // Enquiry list tab + }, + ); + } catch (e) { + // Handle navigation error silently + debugPrint('Navigation error: $e'); + } + }, + child: const Text( + 'View more', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.thridprimary, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + height: 220, + child: ListView.builder( + itemCount: services.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final service = services[index]; + return Container( + width: 162, + margin: const EdgeInsets.only(right: 12), + child: GestureDetector( + onTap: () { + try { + Get.toNamed( + RouterConts.vendorserviceupload, + arguments: service, + ); + } catch (e) { + // Handle navigation error silently + print( + 'Navigation error: $e', + ); // Optional: for debugging + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 203, + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: + (service.images1?.isNotEmpty ?? false) + ? Image.network( + service.images1!.first, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: + ( + context, + error, + stackTrace, + ) => Container( + color: Colors.grey[200], + child: const Center( + child: Icon( + Icons.image_not_supported, + size: 50, + color: Colors.grey, + ), + ), + ), + ) + : Container( + color: Colors.grey[200], + child: const Center( + child: Icon( + Icons.image_not_supported, + size: 50, + color: Colors.grey, + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(20), + ), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 5, + sigmaY: 5, + ), + child: Container( + padding: const EdgeInsets.all(12), + color: Colors.black.withOpacity(0.3), + child: Center( + child: Text( + service.serviceName ?? 'Service', + style: const TextStyle( + color: Colors.white, + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 15, + height: 24.43 / 18, + letterSpacing: 0.89, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (err, stack) => const SizedBox.shrink(), + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final bannerAsyncValue = ref.watch(bannerListProvider); + final bookingsAsyncValue = ref.watch(vendorbookingdetailsProvider); + final profileData = ref.watch(profilegetvendorProvider); + final vendorServiceAsyncValue = ref.watch(vendorserviceProvider); + final expiredPlan = ref.watch(vendorexpiredPlanProvider); + final enquiryListAsync = ref.watch(enquriylistProvider); + final profiles = profileData.value ?? []; + return Scaffold( + key: _scaffoldKey, + drawer: DrawerMenuVendor( + userName: profiles.isNotEmpty + ? (profiles.first.name ?? "Vendor") + : "Vendor", + userImage: profiles.isNotEmpty + ? (profiles.first.profilePic1 ?? "") + : "", + ), + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + // Header section + Padding( + padding: const EdgeInsets.only(left: 16, right: 24, top: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + try { + _scaffoldKey.currentState?.openDrawer(); + } catch (e) { + // Handle drawer error silently + } + }, + child: Image.asset( + AppAssets.menu, + height: 40, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.menu, size: 40); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Welcome', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20, + height: 1.0, + letterSpacing: 0.2372, + color: Colors.black, + ), + ), + SizedBox(height: 7), + Text( + '👋 ${profiles.isNotEmpty ? (profiles.first.name) : "vendor"}', + style: TextStyle( + fontFamily: 'Gilroy-Medium', + fontWeight: FontWeight.w400, + fontSize: 13.48, + height: 1.0, + letterSpacing: 0.2696, + color: Colors.black, + ), + ), + ], + ), + ), + const Spacer(), + Consumer( + builder: (context, ref, _) { + final count = ref.watch(notificationCountProvider); + + return Stack( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.lightGrey, + shape: BoxShape.circle, + ), + child: IconButton( + icon: const Icon( + Icons.notifications_none, + color: Colors.black, + ), + onPressed: () async { + final prefs = + await SharedPreferences.getInstance(); + final vendorId = + prefs.getString('vendor_id') ?? ''; + + // Reset badge when opened + ref + .read( + notificationCountProvider.notifier, + ) + .state = + 0; + + Get.to( + () => VendorController( + child: NotificationPage( + type: 2, + id: vendorId, + ), + ), + ); + }, + ), + ), + if (count > 0) + Positioned( + right: 4, + top: 4, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 20, + minHeight: 20, + ), + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), + + // Banner Carousel + _buildBannerSection(bannerAsyncValue), + const SizedBox(height: 30), + + // Expired Plan Section + _buildExpiredPlanSection(expiredPlan), + const SizedBox(height: 30), + + // Bookings Section + _buildBookingsSection(bookingsAsyncValue), + + // Enquiry Section + _buildEnquirySection(enquiryListAsync), + + const SizedBox(height: 16), + + // Services Section + _buildServicesSection(vendorServiceAsyncValue), + + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} diff --git a/lib/view/vendor_main_screens/vendor_maincontoller.dart b/lib/view/vendor_main_screens/vendor_maincontoller.dart new file mode 100644 index 0000000..9262596 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_maincontoller.dart @@ -0,0 +1,356 @@ +import 'dart:convert'; +import 'dart:ui'; +import 'dart:math' as math; + +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; + +class VendorController extends ConsumerStatefulWidget { + final Widget child; + final int? initialBottomIndex; + + const VendorController({ + super.key, + required this.child, + this.initialBottomIndex, + }); + + @override + ConsumerState createState() => _VendorControllerState(); +} + +class _VendorControllerState extends ConsumerState + with SingleTickerProviderStateMixin { + int _selectedIndex = 0; + bool _showMenu = false; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + + // Set bottom navigation index + if (widget.initialBottomIndex != null) { + _selectedIndex = widget.initialBottomIndex!; + } else { + // Auto-detect based on current route + final currentRoute = Get.currentRoute; + if (currentRoute == RouterConts.vendorhomepage) { + _selectedIndex = 0; + } else if (currentRoute == RouterConts.vendorpackage) { + _selectedIndex = 1; + } else if (currentRoute == RouterConts.vendorcategory) { + _selectedIndex = 2; + } else if (currentRoute == RouterConts.vendorhistory) { + _selectedIndex = 3; + } + } + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + } + + void _toggleMenu() { + setState(() { + _showMenu = !_showMenu; + _showMenu + ? _animationController.forward() + : _animationController.reverse(); + }); + } + + void _onItemTapped(int index, BuildContext context) { + if (_selectedIndex == index) return; + + switch (index) { + case 0: + Get.offAllNamed(RouterConts.vendorhomepage); + break; + case 1: + Get.offAllNamed(RouterConts.vendorpackage); + break; + case 2: + Get.offAllNamed(RouterConts.vendorcategory); + break; + case 3: + // Navigate to vendor history with default tab + Get.offAllNamed( + RouterConts.vendorhistory, + arguments: { + 'historyTab': 0, // Default to first tab + }, + ); + break; + } + } + + void changeSelectedIndex(int index) { + setState(() { + _selectedIndex = index; + }); + } + + // Handle back button press + bool _onWillPop() { + if (_showMenu) { + // If menu is open, close it first + _toggleMenu(); + return false; + } else if (_selectedIndex != 0) { + // If not on home tab, go to home tab + _onItemTapped(0, context); + return false; + } else { + // If on home tab, allow app to exit + return true; + } + } + + // Check if current route should show FAB + bool _shouldShowFAB() { + final currentRoute = Get.currentRoute; + // Hide FAB on edit profile and other specific routes + final hideFABRoutes = [ + '/vendor-edit-profile', // Add your edit profile route here + '/profile-edit', + // Add other routes where you don't want FAB + ]; + + return !hideFABRoutes.contains(currentRoute); + } + + @override + Widget build(BuildContext context) { + final isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0; + final currentRoute = Get.currentRoute; + + return PopScope( + canPop: _selectedIndex == 0 && !_showMenu, + onPopInvoked: (bool didPop) { + if (!didPop) { + _onWillPop(); + } + }, + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + InheritedVendorIndexController( + changeIndex: changeSelectedIndex, + child: widget.child, + ), + if (_showMenu && !isKeyboardOpen) + AnimatedOpacity( + opacity: 1.0, + duration: const Duration(milliseconds: 300), + child: GestureDetector( + onTap: _toggleMenu, + child: Container( + alignment: Alignment.center, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), + child: Container( + color: Colors.black.withOpacity(0.2), + width: double.infinity, + height: double.infinity, + ), + ), + ), + ), + ), + if (_showMenu && !isKeyboardOpen) + Positioned( + bottom: 60, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 110), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildMenuOption( + icon: Icons.person, + label: 'User', + color: AppColors.primary, + onTap: () { + _toggleMenu(); + Get.offNamed(RouterConts.homescreen); + }, + ), + _buildMenuOption( + icon: Icons.person, + label: 'Profile', + color: AppColors.primary, + onTap: () { + _toggleMenu(); + Get.toNamed(RouterConts.profilemainvendor); + }, + ), + ], + ), + ), + ), + ], + ), + floatingActionButton: (isKeyboardOpen || !_shouldShowFAB()) + ? null + : FloatingActionButton( + // 🔧 FIXED: Dynamic hero tag based on route and timestamp + heroTag: + "vendor_controller_fab_${currentRoute}_${DateTime.now().millisecondsSinceEpoch}", + onPressed: _toggleMenu, + shape: const CircleBorder(), + backgroundColor: AppColors.primary, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.rotate( + angle: _animationController.value * 0.75 * math.pi, + child: Icon( + _animationController.value > 0.5 + ? Icons.close + : Icons.add, + color: Colors.white, + ), + ); + }, + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: BottomAppBar( + notchMargin: 8, + color: Colors.white, + elevation: 8, + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _bottomNavItem(0, AppAssets.home), + _bottomNavItem(1, AppAssets.package), + const Expanded(child: SizedBox()), + _bottomNavItem(2, AppAssets.categories), + _bottomNavItem(3, AppAssets.history), + ], + ), + ), + ), + ), + ); + } + + Widget _bottomNavItem(int index, String assetPath) { + return Expanded( + child: InkWell( + onTap: () => _onItemTapped(index, context), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + assetPath, + color: _selectedIndex == index ? AppColors.primary : Colors.grey, + width: 26, + height: 29, + ), + const SizedBox(height: 4), + Container( + height: 3, + width: 30, + decoration: BoxDecoration( + color: _selectedIndex == index + ? AppColors.primary + : Colors.transparent, + borderRadius: BorderRadius.circular(2), + ), + ), + ], + ), + ), + ); + } + + Widget _buildMenuOption({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return ScaleTransition( + scale: CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 3, + spreadRadius: 1, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon(icon, color: Colors.white), + onPressed: onTap, + padding: EdgeInsets.zero, + ), + ), + const SizedBox(height: 5), + Text( + label, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} + +class InheritedVendorIndexController extends InheritedWidget { + final void Function(int) changeIndex; + + const InheritedVendorIndexController({ + super.key, + required this.changeIndex, + required super.child, + }); + + static InheritedVendorIndexController? of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(InheritedVendorIndexController oldWidget) { + return oldWidget.changeIndex != changeIndex; + } +} diff --git a/lib/view/vendor_main_screens/vendor_package_page.dart b/lib/view/vendor_main_screens/vendor_package_page.dart new file mode 100644 index 0000000..312f4f9 --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_package_page.dart @@ -0,0 +1,382 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/model/package_model.dart'; +import 'package:bookmywages/model/plan_sucess_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class VendorPackagePage extends ConsumerStatefulWidget { + const VendorPackagePage({super.key}); + + @override + ConsumerState createState() => _VendorPackagePageState(); +} + +class _VendorPackagePageState extends ConsumerState { + bool isLoading = false; + @override + Widget build(BuildContext context) { + final packageAsyncValue = ref.watch(pacakgeProvider); + final indexController = InheritedVendorIndexController.of(context); + // Get screen size for responsive calculations + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.width < 360; + + return PopScope( + onPopInvoked: (didPop) { + if (didPop) { + // Reset main controller index to 0 when going back + final indexController = InheritedVendorIndexController.of(context); + indexController?.changeIndex(0); + } + }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + + centerTitle: true, + title: Text( + "Subscription Plan", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: isSmallScreen ? 16 : 18, + ), + ), + ), + extendBodyBehindAppBar: true, + body: Container( + height: double.infinity, + width: double.infinity, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage(AppAssets.background), + fit: BoxFit.cover, + ), + ), + child: SafeArea( + // Added SafeArea to ensure content doesn't overlap with system UI + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: + screenSize.width * 0.04, // Responsive horizontal padding + vertical: + screenSize.height * 0.02, // Responsive vertical padding + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Responsive spacing based on screen height + SizedBox(height: screenSize.height * 0.05), + Text( + "Choose your right plan", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w400, + fontSize: isSmallScreen + ? 24 + : 32, // Smaller font for small screens + height: 1.0, + letterSpacing: 0.32, + color: Colors.black, + ), + ), + SizedBox(height: screenSize.height * 0.01), + Text( + "Make upgrade your plan and get more advantages of services", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: isSmallScreen + ? 13 + : 15, // Smaller font for small screens + height: 1.71, + letterSpacing: 0.15, + color: Colors.black, + ), + ), + SizedBox(height: screenSize.height * 0.02), + packageAsyncValue.when( + data: (packages) { + final filteredPackages = packages + .where((pkg) => pkg.type == 2) + .toList(); + + // Responsive container height based on screen size + return SizedBox( + height: + screenSize.height * + 0.3, // 35% of screen height instead of fixed 250 + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: filteredPackages.length, + itemBuilder: (context, index) { + final PackageModel package = + filteredPackages[index]; + + // Calculate responsive card width (based on screen width) + final cardWidth = screenSize.width * 0.75 > 250 + ? 250.0 + : screenSize.width * 0.75; + + return Container( + width: cardWidth, + margin: EdgeInsets.symmetric( + horizontal: screenSize.width * 0.02, + vertical: screenSize.height * 0.01, + ), + decoration: BoxDecoration( + color: const Color(0xFF334E95), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: const Color(0xFF334E95), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x40828282), + offset: Offset(0, 5), + blurRadius: 4, + ), + ], + ), + child: Padding( + padding: EdgeInsets.all( + screenSize.width * 0.04, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + package.name, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: isSmallScreen + ? 16 + : 18, // Smaller font for small screens + height: 1.0, + letterSpacing: 0.32, + color: Colors.white, + ), + ), + ), + SizedBox(height: screenSize.height * 0.01), + _buildFeatureRow( + package.description, + isSmallScreen, + ), + SizedBox(height: screenSize.height * 0.02), + _buildFeatureRow( + getFormattedDuration(package.duration), + isSmallScreen, + ), + SizedBox(height: screenSize.height * 0.02), + _buildFeatureRow( + '${package.noOfService} book service', + isSmallScreen, + ), + const Divider(color: Colors.white), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + package.price, + style: TextStyle( + fontSize: isSmallScreen ? 14 : 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + GestureDetector( + onTap: () => _subscribeToPlan( + package, + indexController, + ), + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_forward, + color: Colors.black, + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ); + }, + loading: () => + const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Text( + 'Error loading packages: $error', + style: const TextStyle(color: Colors.red), + ), + ), + ), + SizedBox(height: screenSize.height * 0.01), + ], + ), + ), + ), + ), + ), + ); + } + + // Extracted feature row widget for better code organization + Widget _buildFeatureRow(String text, bool isSmallScreen) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar(radius: 8, backgroundColor: Color(0xFFF9E369)), + const SizedBox(width: 6), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: isSmallScreen ? 12 : 14, + color: Colors.white, + ), + ), + ), + ], + ); + } + + Future _subscribeToPlan( + PackageModel package, + InheritedVendorIndexController? indexController, + ) async { + setState(() { + isLoading = true; + }); + + try { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('vendor_id') ?? ''; + + final model = PlanSuccessModel( + userId: userId, + planId: package.id.toString(), + duration: package.duration, + type: package.type, + ); + + final result = await ref + .read(planSuccessProvider(model).future) + .catchError((e) { + Fluttertoast.showToast( + msg: 'Error: ${e.toString()}', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + return false; + }); + + if (result) { + if (mounted) { + // Show success popup + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Success'), + content: const Text('Plan subscribed successfully!'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + + Get.offNamed(RouterConts.vendorcategory, arguments: 2); + }, + ), + ], + ); + }, + ); + } + } else { + if (mounted) { + // Show error popup + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Error'), + content: const Text('Failed to subscribe to plan'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + } + } catch (error) { + if (mounted) { + // Show error popup + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Error'), + content: Text('Failed to subscribe: ${error.toString()}'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + } finally { + if (mounted) { + setState(() { + isLoading = false; + }); + } + } + } + + String getFormattedDuration(int duration) { + if (duration < 30) { + return '$duration month'; + } else { + int months = duration ~/ 30; + return '$months month'; + } + } +} diff --git a/lib/view/vendor_main_screens/vendor_register_page.dart b/lib/view/vendor_main_screens/vendor_register_page.dart new file mode 100644 index 0000000..b52f54d --- /dev/null +++ b/lib/view/vendor_main_screens/vendor_register_page.dart @@ -0,0 +1,374 @@ +import 'package:bookmywages/consts_widgets/app_assets.dart'; +import 'package:bookmywages/consts_widgets/app_colors.dart'; +import 'package:bookmywages/consts_widgets/comman_button.dart'; +import 'package:bookmywages/consts_widgets/comman_textformfiled.dart'; +import 'package:bookmywages/model/vendor_model/vendorregister_model.dart'; +import 'package:bookmywages/routers/consts_router.dart'; +import 'package:bookmywages/viewmodel/api_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; + +class VendorRegisterPage extends ConsumerStatefulWidget { + const VendorRegisterPage({super.key}); + + @override + ConsumerState createState() => _VendorRegisterPageState(); +} + +class _VendorRegisterPageState extends ConsumerState { + final _nameController = TextEditingController(); + final _numberController = TextEditingController(); + final _emailController = TextEditingController(); + final _locationController = TextEditingController(); + final _pincodeController = TextEditingController(); + final _aadharController = TextEditingController(); + final _panController = TextEditingController(); + final _licenseController = TextEditingController(); + final _passportController = TextEditingController(); + + String? _nameError; + String? _numberError; + String? _emailError; + + void _validateName(String value) { + setState(() => _nameError = value.isEmpty ? 'Name is required' : null); + } + + void _validateNumber(String value) { + setState(() { + if (value.isEmpty) { + _numberError = 'Phone number is required'; + } else if (!RegExp(r'^[0-9]{10}$').hasMatch(value)) { + _numberError = 'Enter a valid 10-digit phone number'; + } else { + _numberError = null; + } + }); + } + + void _validateEmail(String value) { + setState(() { + if (value.isEmpty) { + _emailError = 'Email is required'; + } else if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + _emailError = 'Enter a valid email'; + } else { + _emailError = null; + } + }); + } + + @override + void dispose() { + _nameController.dispose(); + _numberController.dispose(); + _emailController.dispose(); + _locationController.dispose(); + _pincodeController.dispose(); + _aadharController.dispose(); + _panController.dispose(); + _licenseController.dispose(); + _passportController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final double horizontalPadding = screenSize.width * 0.05; + final double imageHeight = screenSize.height * 0.45; + + return Scaffold( + backgroundColor: AppColors.secondprimary, + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Column( + children: [ + Image.asset( + AppAssets.login, + width: screenSize.width, + height: imageHeight, + fit: BoxFit.cover, + ), + Transform.translate( + offset: const Offset(0, -100), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.73), + border: Border.all( + width: 1.09, + color: const Color(0xFFA39898), + ), + boxShadow: const [ + BoxShadow( + offset: Offset(0, -4), + blurRadius: 12.3, + color: Color(0x66E0E0E0), + ), + BoxShadow( + offset: Offset(0, 4), + blurRadius: 4, + color: Color(0x66000000), + ), + ], + color: Colors.white, + ), + child: Padding( + padding: EdgeInsets.all(screenSize.width * 0.05), + child: Column( + children: [ + const SizedBox(height: 20), + const Text( + "Vendor Registration", + style: TextStyle( + fontSize: 25, + fontFamily: "Gilroy-ExtraBold", + fontWeight: FontWeight.w800, + height: 1.0, + letterSpacing: 0.64, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _nameController, + hintText: 'Enter your name', + prefixIcon: const Icon( + Icons.person, + color: AppColors.hittext, + ), + errorText: _nameError, + onChanged: _validateName, + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _numberController, + hintText: 'Enter your number', + prefixIcon: const Icon( + Icons.phone_android, + color: AppColors.hittext, + ), + errorText: _numberError, + onChanged: _validateNumber, + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _emailController, + hintText: 'Enter your E-mail Id', + prefixIcon: const Icon( + Icons.email, + color: AppColors.hittext, + ), + errorText: _emailError, + onChanged: _validateEmail, + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _locationController, + hintText: 'Location', + prefixIcon: const Icon( + Icons.location_on, + color: AppColors.hittext, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _pincodeController, + hintText: 'Pin code', + prefixIcon: const Icon( + Icons.pin_drop, + color: AppColors.hittext, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _aadharController, + hintText: 'Aadhar card number', + prefixIcon: const Icon( + Icons.badge, + color: AppColors.hittext, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _panController, + hintText: 'Pan card number', + prefixIcon: const Icon( + Icons.fact_check, + color: AppColors.hittext, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _licenseController, + hintText: 'Driving License number', + prefixIcon: const Icon( + Icons.directions_car, + color: AppColors.hittext, + ), + ), + SizedBox(height: screenSize.height * 0.03), + + CommonTextFormField( + controller: _passportController, + hintText: 'Passport number', + prefixIcon: const Icon( + Icons.flight, + color: AppColors.hittext, + ), + ), + SizedBox(height: screenSize.height * 0.04), + + CommanButton( + text: 'Submit', + textStyle: const TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w800, + fontSize: 20, + height: 1.0, + color: AppColors.secondprimary, + ), + onPressed: () async { + _validateName(_nameController.text); + _validateNumber(_numberController.text); + _validateEmail(_emailController.text); + + if (_nameError == null && + _numberError == null && + _emailError == null) { + final model = VendorRegisterModel( + name: _nameController.text.trim(), + number: _numberController.text.trim(), + email: _emailController.text.trim(), + location: _locationController.text.trim(), + pincode: _pincodeController.text.trim(), + aadharCard: _aadharController.text.trim(), + panCard: _panController.text.trim(), + drivingLicense: _licenseController.text.trim(), + passport: _passportController.text.trim(), + ); + + try { + final response = await ref.read( + vendorRegisterFutureProvider(model).future, + ); + print('API Response: $response'); + + _showVerificationDialog(context); + } catch (e) { + print("Registration Error: $e"); + Fluttertoast.showToast( + msg: "Error: $e", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); + } + } + }, + isPrimary: true, + backgroundColor: AppColors.primary, + textColor: AppColors.secondprimary, + width: 230, + ), + const SizedBox(height: 30), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _showVerificationDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, // Prevent closing by tapping outside + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + side: const BorderSide(color: Colors.white, width: 1), + ), + elevation: 10, + backgroundColor: Colors.white, + child: Container( + width: 365, + height: 305, + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Verification', + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 32, + height: 1.0, + letterSpacing: 0.32, + ), + ), + const SizedBox(height: 20), + const Text( + 'Still your account will not verify by Admin, please wait with us.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Gilroy-Bold', + fontWeight: FontWeight.w700, + fontSize: 20.23, + height: 1.74, + letterSpacing: 0.2023, + ), + ), + const SizedBox(height: 30), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(29.36), + ), + padding: const EdgeInsets.symmetric( + horizontal: 50, + vertical: 15, + ), + ), + onPressed: () { + Navigator.of(context).pop(); // Close dialog + Get.toNamed(RouterConts.homescreen); + }, + child: const Text( + 'OK', + style: TextStyle( + fontFamily: 'Gilroy-Black', + fontWeight: FontWeight.w400, + fontSize: 20.29, + height: 1.0, + letterSpacing: 0.4058, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/viewmodel/api_controller.dart b/lib/viewmodel/api_controller.dart new file mode 100644 index 0000000..8b072a8 --- /dev/null +++ b/lib/viewmodel/api_controller.dart @@ -0,0 +1,490 @@ +import 'dart:io'; + +import 'package:bookmywages/model/Banner_model.dart'; +import 'package:bookmywages/model/Categories_model.dart'; +import 'package:bookmywages/model/booking_modify_model.dart'; +import 'package:bookmywages/model/cancel_booking.dart'; +import 'package:bookmywages/model/detail_page_model.dart'; +import 'package:bookmywages/model/enquriy_list_model.dart'; +import 'package:bookmywages/model/enquriy_model.dart'; +import 'package:bookmywages/model/expired_plan_model.dart'; +import 'package:bookmywages/model/get_review_model.dart'; +import 'package:bookmywages/model/most_popular_model.dart'; +import 'package:bookmywages/model/notification_model.dart'; +import 'package:bookmywages/model/otp_model.dart'; +import 'package:bookmywages/model/package_model.dart'; +import 'package:bookmywages/model/payment_details_model.dart'; +import 'package:bookmywages/model/plan_sucess_model.dart'; +import 'package:bookmywages/model/profile_get_model.dart'; +import 'package:bookmywages/model/service_model.dart'; +import 'package:bookmywages/model/subcategory_model.dart'; +import 'package:bookmywages/model/user_booking_details.dart'; +import 'package:bookmywages/model/vendor_model/terms_and_conditions_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_booking_model.dart' + show VendorBookingModel; +import 'package:bookmywages/model/vendor_model/vendor_booking_status.dart'; +import 'package:bookmywages/model/vendor_model/vendor_catgories_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_profile_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_service_model.dart'; +import 'package:bookmywages/model/vendor_model/vendor_serviceupload_model.dart'; +import 'package:bookmywages/model/vendor_model/vendorregister_model.dart'; +import 'package:bookmywages/view/auth/auth_repository.dart'; +import 'package:bookmywages/view/user_main_screens/notification_page.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../model/login_model.dart'; + +import '../viewmodel/consts_api.dart'; + +final loginRepositoryProvider = Provider((ref) => AuthRepository()); + +final loginFutureProvider = FutureProvider.family + .autoDispose, LoginModel>((ref, loginModel) async { + final repo = ref.read(loginRepositoryProvider); + return repo.loginUser(ConstsApi.login, loginModel); + }); + +final signupFutureProvider = FutureProvider.family + .autoDispose, Map>(( + ref, + signupData, + ) async { + final repo = ref.read(loginRepositoryProvider); + return repo.registerUser(ConstsApi.register, signupData); + }); +// Add this to your providers file +final otpVerificationProvider = FutureProvider.family + .autoDispose, OtpModel>((ref, otpModel) async { + final repo = ref.read(loginRepositoryProvider); + return repo.verifyOtp(ConstsApi.otp, otpModel); + }); +final bannerRepositoryProvider = Provider((ref) => BannerRepository()); + +final bannerListProvider = FutureProvider.autoDispose>(( + ref, +) async { + final repo = ref.read(bannerRepositoryProvider); + return repo.fetchBanners(ConstsApi.banner); +}); +final categoryRepositoryProvider = Provider((ref) => CategoryRepository()); + +final categoryListProvider = FutureProvider.autoDispose>(( + ref, +) async { + final repo = ref.read(categoryRepositoryProvider); + return repo.fetchCategories(ConstsApi.catgories); +}); + +final packageRepositoryProvider = Provider((ref) => packageRepository()); + +final pacakgeProvider = FutureProvider.autoDispose>(( + ref, +) async { + final repo = ref.read(packageRepositoryProvider); + return repo.fetchpackage(ConstsApi.package); +}); + +final profilegetRepositoryProvider = Provider((ref) => ProfilegetRepository()); + +final profilegetuserProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(profilegetRepositoryProvider); + return repo.fetchprofileget(ConstsApi.profileget); + }); +final subcategoryRepositoryProvider = Provider( + (ref) => SubcategoryRepository(), +); + +final subcategoryProvider = FutureProvider.family + .autoDispose, String>((ref, categoryId) async { + final repo = ref.read(subcategoryRepositoryProvider); + return repo.fetchSubcategories(ConstsApi.subcat, categoryId); + }); + +final serviceRepositoryProvider = Provider((ref) => ServiceRepository()); + +final serviceProvider = FutureProvider.family + .autoDispose< + List, + ({String categoryId, String? subcategoryId, String? selecttype}) + >((ref, params) async { + final repo = ref.read(serviceRepositoryProvider); + return repo.fetchService( + ConstsApi.service, + params.categoryId, + params.subcategoryId, + params.selecttype, + ); + }); +final profileupdateRepositoryProvider = Provider( + (ref) => ProfileupdateRepository(), +); + +final profileupdateuserProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(profileupdateRepositoryProvider); + return repo.fetchprofileupdate(ConstsApi.upadateprofile); + }); +final getreviewRepositoryProvider = Provider((ref) => GetReviewRepository()); + +final getreviewuserProvider = + FutureProvider.family, String>((ref, serviceId) async { + final repo = ref.read(getreviewRepositoryProvider); + return repo.fetchgetreview(ConstsApi.getreview, serviceId); + }); + +final mostPopularRepositoryProvider = Provider( + (ref) => MostPopularRepository(), +); + +// Create a provider family that accepts a categoryId parameter +final mostPopularProvider = + FutureProvider.family, String>(( + ref, + categoryId, + ) async { + final repo = ref.read(mostPopularRepositoryProvider); + return repo.fetchMostPopular(ConstsApi.mostpopular, categoryId); + }); + +final enquriyupdateRepositoryProvider = Provider((ref) => EnquriyRepository()); + +final enquriyProvider = FutureProvider.autoDispose>(( + ref, +) async { + final repo = ref.read(enquriyupdateRepositoryProvider); + return repo.fetchenquriy(ConstsApi.enquery); +}); + +final enquriyRepositoryProvider = Provider((ref) => enquriylistRepository()); + +final enquriylistProvider = FutureProvider.autoDispose>(( + ref, +) async { + final repo = ref.read(enquriyRepositoryProvider); + return repo.fetchenquirylist(ConstsApi.enquirylist); +}); + +final enquriydeleteRepositoryProvider = Provider( + (ref) => EnquriydeleteRepository(), +); + +final enquriydeleteProvider = FutureProvider.autoDispose.family(( + ref, + id, +) async { + final repo = ref.read(enquriydeleteRepositoryProvider); + return repo.enquriydelete(ConstsApi.enquirydelete, id); +}); + +final paymentlistRepositoryProvider = Provider( + (ref) => PaymentdetailsRepository(), +); + +final paymentdetailsProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(paymentlistRepositoryProvider); + return repo.fetchpayment(ConstsApi.paymentdetails); + }); + +final paymentdeleteRepositoryProvider = Provider( + (ref) => PaymentdeleteRepository(), +); + +final paymentdeleteProvider = FutureProvider.autoDispose.family(( + ref, + id, +) async { + final repo = ref.read(paymentdeleteRepositoryProvider); + return repo.paymentdelete(ConstsApi.paymentdelete, id); +}); + +final changepassRepositoryProvider = Provider( + (ref) => ChangepasswordRepository(), +); + +final changepasswordProvider = FutureProvider.autoDispose.family(( + ref, + password, +) async { + final repo = ref.read(changepassRepositoryProvider); + return repo.changepassword(ConstsApi.changepassword, password); +}); + +final servicebookinguserhistorydetailsRepositoryProvider = Provider( + (ref) => UserhistoryBookingdetailsRepository(), +); + +final userbookinghistorydetailsProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(servicebookinguserhistorydetailsRepositoryProvider); + return repo.fetchhistorybooking(ConstsApi.historybooking); + }); + +final cancelbookingRepositoryProvider = Provider( + (ref) => CancelbookingRepository(), +); + +final cancelbookingProvider = FutureProvider.autoDispose + .family((ref, data) async { + final repo = ref.read(cancelbookingRepositoryProvider); + return repo.cancelbooking( + url: ConstsApi.cancelbooking, + id: data.id, + serviceId: data.serviceId, + type: data.type, + ); + }); +final bookingmodifyRepositoryProvider = Provider( + (ref) => BookingmodifyRepository(), +); + +final modifybookingProvider = FutureProvider.autoDispose + .family((ref, data) async { + final repo = ref.read(bookingmodifyRepositoryProvider); + return repo.bookingmodify( + url: ConstsApi.bookingmodify, + id: data.id, + serviceId: data.serviceId, + servicedate: data.servicedate, + servicetime: data.servicetime, + ); + }); + +final expiredPlanRepositoryProvider = Provider( + (ref) => ExpiredPlanRepository(), +); + +final expiredPlanProvider = FutureProvider.autoDispose(( + ref, +) async { + final repo = ref.read(expiredPlanRepositoryProvider); + return repo.fetchExpiredPlan(ConstsApi.expiredplan); +}); + +final planSuccessRepositoryProvider = Provider( + (ref) => PlanSuccessRepository(), +); + +// Provider for plan success functionality +final planSuccessProvider = FutureProvider.autoDispose + .family((ref, data) async { + final repo = ref.read(planSuccessRepositoryProvider); + return repo.planSuccess( + url: ConstsApi.plansucess, + userId: data.userId, + planId: data.planId, + duration: data.duration, + type: data.type, + ); + }); + +final detailpageRepositoryProvider = Provider((ref) => DetailpageRepository()); + +final detailpageProvider = FutureProvider.family + .autoDispose, String>((ref, serviceid) async { + final repo = ref.read(detailpageRepositoryProvider); + return repo.fetchDetailpage(ConstsApi.detail, int.parse(serviceid)); + }); + +//--------------------vendor flow----------------------------------------- + +final getvendorIdRepositoryProvider = Provider((ref) => GetvendorId()); + +final getvendorIdProvider = FutureProvider.autoDispose((ref) async { + final repo = ref.read(getvendorIdRepositoryProvider); + return repo.fetchGetid(ConstsApi.getvendorid); +}); + +final vendorRegisterRepositoryProvider = Provider((ref) => VendorRegister()); + +// Future provider to call the register API +final vendorRegisterFutureProvider = FutureProvider.family + .autoDispose, VendorRegisterModel>(( + ref, + vendorModel, + ) async { + final repo = ref.read(vendorRegisterRepositoryProvider); + return repo.vendorRegister(ConstsApi.vendorregister, vendorModel); + }); + +final vendorserviceuploadRepositoryProvider = Provider( + (ref) => Vendorserviceupload(), +); + +final vendorserviceFutureProvider = FutureProvider.family + .autoDispose, (VendorServiceUploadModel, List)>(( + ref, + data, + ) async { + final repo = ref.read(vendorserviceuploadRepositoryProvider); + return repo.vendorserviceupload( + ConstsApi.vendorserivceupload, + data.$1, // VendorServiceUploadModel + data.$2, // List images + ); + }); + +final vendorbookingdetailsRepositoryProvider = Provider( + (ref) => VendorbookingdetailsRepository(), +); + +final vendorbookingdetailsProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(vendorbookingdetailsRepositoryProvider); + return repo.fetchvendorbooking(ConstsApi.vendormybooking); + }); + +final changebookingstatusRepositoryProvider = Provider( + (ref) => ChangebookingstatusRepository(), +); + +final changebookingProvider = FutureProvider.autoDispose + .family((ref, data) async { + final repo = ref.read(changebookingstatusRepositoryProvider); + return repo.changebooking( + url: ConstsApi.vendorchangebookingstatus, + id: data.id.toString(), + status: data.status.toString(), + ); + }); + +final vendorcatgoriesRepositoryProvider = Provider( + (ref) => VendorcatgoriesRepository(), +); + +final vendorcatgoriesProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(vendorcatgoriesRepositoryProvider); + return repo.fetchvendorcat(ConstsApi.vendorcat); + }); + +final vendorserviceRepositoryProvider = Provider( + (ref) => VendorserviceRepository(), +); + +final vendorserviceProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(vendorserviceRepositoryProvider); + return repo.fetchvendorservice(ConstsApi.vendormyservice); + }); + +final vendorserviceupdateRepositoryProvider = Provider( + (ref) => Vendorserviceupadate(), +); +final vendorServiceUploadUpdateProvider = FutureProvider.family + .autoDispose, (VendorServiceUploadModel, List)>(( + ref, + data, + ) async { + final repo = ref.read(vendorserviceupdateRepositoryProvider); + return repo.vendorserviceupdate( + ConstsApi.vendorserviceupload, + data.$1, + data.$2, + ); + }); + +final vendorexpiredPlanRepositoryProvider = Provider( + (ref) => VendorExpiredPlanRepository(), +); + +final vendorexpiredPlanProvider = FutureProvider.autoDispose(( + ref, +) async { + final repo = ref.read(vendorexpiredPlanRepositoryProvider); + return repo.fetchvendorExpiredPlan(ConstsApi.expiredplan); +}); +final catDeleteRepositoryProvider = Provider( + (ref) => CatManagementDeleteRepository(), +); + +// Provider for delete operation +final deleteCategoryProvider = FutureProvider.family.autoDispose(( + ref, + categoryId, +) async { + final repo = ref.read(catDeleteRepositoryProvider); + return repo.deleteCategoryManagement( + ConstsApi.catmangementdelete, + categoryId, + ); +}); + +final ServiceDeleteRepositoryProvider = Provider( + (ref) => SerciceDeleteRepository(), +); + +// Provider for delete operation +final deleteserviceProvider = FutureProvider.family.autoDispose(( + ref, + id, +) async { + final repo = ref.read(ServiceDeleteRepositoryProvider); + return repo.deleteService(ConstsApi.servicedelete, id); +}); + +final profilegetvendorRepositoryProvider = Provider( + (ref) => ProfilegetvendorRepository(), +); + +final profilegetvendorProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(profilegetvendorRepositoryProvider); + return repo.fetchprofilegetvendor(ConstsApi.vendorprofileget); + }); + +final profileupdatevendorRepositoryProvider = Provider( + (ref) => ProfileupdatevendorRepository(), +); + +final profileupdatevendorProvider = + FutureProvider.autoDispose>((ref) async { + final repo = ref.read(profileupdatevendorRepositoryProvider); + return repo.fetchProfileupdatevendor(ConstsApi.upadatevendorprofile); + }); + +final termsAndConditionsRepositoryProvider = Provider( + (ref) => TermsAndConditionsRepository(), +); + +final termsAndConditionsProvider = + FutureProvider.autoDispose((ref) async { + final repo = ref.read(termsAndConditionsRepositoryProvider); + return repo.fetchTermsAndCondition(ConstsApi.termsandcondition); + }); + +final privacypolicyRepositoryProvider = Provider( + (ref) => PrivacypolicyRepository(), +); + +final privacypolicyProvider = + FutureProvider.autoDispose((ref) async { + final repo = ref.read(privacypolicyRepositoryProvider); + return repo.fetchPrivacypolicy(ConstsApi.privacypolicy); + }); + +final notificationRepositoryProvider = Provider( + (ref) => NotificationRepository(), +); + +final notificationProvider = FutureProvider.family + .autoDispose, ({int type, String userId})>(( + ref, + params, + ) async { + final repo = ref.read(notificationRepositoryProvider); + return repo.fetchNotification(type: params.type, userId: params.userId); + }); + +Future updateNotificationCount( + WidgetRef ref, { + required int type, + required String userId, +}) async { + final repo = ref.read(notificationRepositoryProvider); + final notifications = await repo.fetchNotification( + type: type, + userId: userId, + ); + ref.read(notificationCountProvider.notifier).state = notifications.length; +} diff --git a/lib/viewmodel/consts_api.dart b/lib/viewmodel/consts_api.dart new file mode 100644 index 0000000..fd51eb0 --- /dev/null +++ b/lib/viewmodel/consts_api.dart @@ -0,0 +1,55 @@ +class ConstsApi { + static const String baseUrl = + "https://www.demo603.amrithaa.com/bookmywages/admin/public/api"; + + // Example endpoints + static const String login = "$baseUrl/login"; + static const String register = "$baseUrl/register"; + static const String otp = "$baseUrl/verifyotp"; + static const String banner = "$baseUrl/getslider"; + static const String catgories = "$baseUrl/getcategory"; + static const String trendingserivce = "$baseUrl/trendingservice"; + static const String package = "$baseUrl/getplan"; + static const String subcat = "$baseUrl/getsubcategory"; + static const String service = "$baseUrl/filterservice"; + static const String detail = "$baseUrl/getservice"; + static const String profileget = "$baseUrl/getuserprofile"; + static const String upadateprofile = "$baseUrl/updateuserprofile"; + static const String getreview = "$baseUrl/getreview"; + static const String mostpopular = "$baseUrl/popularservice"; + static const String enquery = "$baseUrl/enquiry"; + static const String bookservice = "$baseUrl/bookservice"; + static const String updatereview = "$baseUrl/addreview"; + static const String enquirylist = "$baseUrl/userenquiry"; + static const String enquirydelete = "$baseUrl/deleteenquiry"; + static const String paymentdetails = "$baseUrl/paymenthistory"; + static const String paymentdelete = "$baseUrl/deletepaymentdetail"; + static const String changepassword = "$baseUrl/changepassword"; + static const String historybooking = "$baseUrl/mybooking"; + static const String cancelbooking = "$baseUrl/cancelbooking"; + static const String bookingmodify = "$baseUrl/bookingmodify"; + static const String expiredplan = "$baseUrl/getlastexpiredplan"; + static const String plansucess = "$baseUrl/subscription"; + + //vendor flow + static const String getvendorid = "$baseUrl/getvendordetail"; + static const String vendorregister = "$baseUrl/vendorregister"; + + static const String vendorserivceupload = "$baseUrl/addservice"; + static const String vendormybooking = "$baseUrl/vendorbooking"; + static const String vendorchangebookingstatus = + "$baseUrl/changebookingstatus"; + + static const String vendorcat = "$baseUrl/getcategoryvendor"; + + static const String imageupload = "$baseUrl/uploadimage"; + static const String vendormyservice = "$baseUrl/myservice"; + static const String vendorserviceupload = "$baseUrl/updateservice"; + static const String catmangementdelete = "$baseUrl/deletecategory"; + static const String servicedelete = "$baseUrl/deleteservice"; + static const String vendorprofileget = "$baseUrl/getvendorprofile"; + static const String upadatevendorprofile = "$baseUrl/updatevendorprofile"; + static const String termsandcondition = "$baseUrl/getTermsandCondition"; + static const String privacypolicy = "$baseUrl/getPrivacyPolicy"; + static const String notification = "$baseUrl/getnotification"; +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..bed54ee --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "bookmywages") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.bookmywages") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..64a0ece --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2db3c22 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..3678f09 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "bookmywages"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "bookmywages"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..eaa7c2c --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos +import flutter_inappwebview_macos +import path_provider_foundation +import shared_preferences_foundation +import sqflite_darwin +import video_player_avfoundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c0c5310 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* bookmywages.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "bookmywages.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* bookmywages.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* bookmywages.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bookmywages.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bookmywages"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bookmywages.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bookmywages"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bookmywages.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bookmywages"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..5b43362 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..d97da91 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = bookmywages + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.bookmywages + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +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/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..4019eee --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1274 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f + url: "https://pub.dev" + source: hosted + version: "82.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" + url: "https://pub.dev" + source: hosted + version: "7.4.5" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + 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" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: c820e918863a03544aac68eaf61e17c8a6126b663d7cad24a8fd3657a1e6be61 + url: "https://pub.dev" + source: hosted + version: "10.1.2" + auto_route_generator: + dependency: "direct main" + description: + name: auto_route_generator + sha256: "2a5b5bf9c55d4a2098931037dac90921a4663808aed494bb4f134d82d46cb8ec" + url: "https://pub.dev" + source: hosted + version: "10.2.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct main" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.dev" + source: hosted + version: "8.12.0" + 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" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff + url: "https://pub.dev" + source: hosted + version: "5.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.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" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + 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_style: + dependency: transitive + description: + name: dart_style + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + dotted_line: + dependency: "direct main" + description: + name: dotted_line + sha256: "41e3d655939559815daa1370fc1e07673a205fa628cf40ce3af45d90029a77b6" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + 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" + 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_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + 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_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + 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: "direct main" + description: + name: fluttertoast + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + url: "https://pub.dev" + source: hosted + version: "8.2.12" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get: + dependency: "direct main" + description: + name: get + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + url: "https://pub.dev" + source: hosted + version: "4.7.2" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + 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" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + 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" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: "8efb801a43b8454f214e70b173c5d81cb57795f70564c728eda352b396d8f8f6" + url: "https://pub.dev" + source: hosted + version: "0.1.0-alpha.11" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + 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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + 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" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2 + url: "https://pub.dev" + source: hosted + version: "5.0.2" + 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" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.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" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + 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" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + 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" + 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: + dependency: transitive + description: + name: test + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + url: "https://pub.dev" + source: hosted + version: "1.26.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + test_core: + dependency: transitive + description: + name: test_core + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + url: "https://pub.dev" + source: hosted + version: "0.6.11" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + 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" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + youtube_player_flutter: + dependency: "direct main" + description: + name: youtube_player_flutter + sha256: "924a4099b052119a42bbd9491be8891ab3c2e05074f3649ce73b596f3f440f19" + url: "https://pub.dev" + source: hosted + version: "9.1.2" +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..c36f926 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,142 @@ +name: bookmywages +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + flutter_riverpod: ^2.6.1 + auto_route: ^10.1.0+1 + build_runner: ^2.4.15 + auto_route_generator: ^10.2.3 + get: ^4.7.2 + fluttertoast: ^8.2.12 + image_picker: ^1.1.2 + path_provider: ^2.1.5 + shared_preferences: ^2.5.3 + pinput: ^5.0.1 + carousel_slider: ^5.1.1 + intl: ^0.20.2 + cached_network_image: ^3.4.1 + youtube_player_flutter: ^9.1.1 + video_player: ^2.10.0 + dotted_line: ^3.2.3 + flutter_rating_bar: ^4.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: Gilroy + fonts: + - asset: fonts/Gilroy-Thin.ttf + weight: 100 + - asset: fonts/Gilroy-ThinItalic.ttf + weight: 100 + style: italic + - asset: fonts/Gilroy-UltraLight.ttf + weight: 200 + - asset: fonts/Gilroy-UltraLightItalic.ttf + weight: 200 + style: italic + - asset: fonts/Gilroy-Light.ttf + weight: 300 + - asset: fonts/Gilroy-LightItalic.ttf + weight: 300 + style: italic + - asset: fonts/Gilroy-Regular.ttf + weight: 400 + - asset: fonts/Gilroy-RegularItalic.ttf + weight: 400 + style: italic + - asset: fonts/Gilroy-Medium.ttf + weight: 500 + - asset: fonts/Gilroy-MediumItalic.ttf + weight: 500 + style: italic + - asset: fonts/Gilroy-SemiBold.ttf + weight: 600 + - asset: fonts/Gilroy-SemiBoldItalic.ttf + weight: 600 + style: italic + - asset: fonts/Gilroy-Bold.ttf + weight: 700 + - asset: fonts/Gilroy-BoldItalic.ttf + weight: 700 + style: italic + - asset: fonts/Gilroy-ExtraBold.ttf + weight: 800 + - asset: fonts/Gilroy-ExtraBoldItalic.ttf + weight: 800 + style: italic + - asset: fonts/Gilroy-Heavy.ttf + weight: 900 + - asset: fonts/Gilroy-HeavyItalic.ttf + weight: 900 + style: italic + + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..3e76c52 --- /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 example, 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:bookmywages/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const BookMyWage()); + + // 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); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..c7c763a --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + bookmywages + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..c1a7542 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "bookmywages", + "short_name": "bookmywages", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..561d2fa --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(bookmywages LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "bookmywages") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..5a3056d --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..1135f2b --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + flutter_inappwebview_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..af9cd9d --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "bookmywages" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "bookmywages" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "bookmywages.exe" "\0" + VALUE "ProductName", "bookmywages" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..ccc2b7e --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"bookmywages", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_