diff --git a/.env b/.env new file mode 100644 index 0000000..10147d1 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +API_KEY=AIzaSyCNpUV_4cMEL9QZx7NESXK7QAlRjRGwx_Y +AUTH_DOMAIN=prosapp-5747a.firebaseapp.com +PROJECT_ID=prosapp-5747a +STORAGE_BUCKET=prosapp-5747a.appspot.com +MESSAGING_SENDER_ID=245204384533 +APP_ID=1:245204384533:web:1b8f0b4c1d9ae21d9f871c +MEASUREMENT_ID=G-EVBEQRZEDY diff --git a/.firebaserc b/.firebaserc deleted file mode 100644 index 789ed62..0000000 --- a/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "prosapp-5747a" - } -} diff --git a/.gitignore b/.gitignore index 24476c5..29a3a50 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ migrate_working_dir/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.packages .pub-cache/ .pub/ /build/ diff --git a/.metadata b/.metadata index af46018..cbf1dc0 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # 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. +# This file should be version controlled and should not be manually edited. version: - revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - channel: stable + revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - platform: android - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - platform: ios - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - platform: linux - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - platform: macos - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - platform: web - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - platform: windows - create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 - base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 # User provided section diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 0fc241c..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - // Use IntelliSense para saber los atributos posibles. - // Mantenga el puntero para ver las descripciones de los existentes atributos. - // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "prosappco", - "request": "launch", - "type": "dart" - }, - { - "name": "prosappco (profile mode)", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "prosappco (release mode)", - "request": "launch", - "type": "dart", - "flutterMode": "release" - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index 29b6900..dde756d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# prosappco +# prosapp_web_app A new Flutter project. diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..0d29021 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,8 +13,7 @@ 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-lang.github.io/linter/lints/index.html. + # 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 diff --git a/android/app/build.gradle b/android/app/build.gradle index 922c90f..4c27519 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,78 +1,58 @@ +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" +} + def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') +def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> + localPropertiesFile.withReader("UTF-8") { reader -> localProperties.load(reader) } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = "1" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') +def flutterVersionName = localProperties.getProperty("flutter.versionName") if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = "1.0" } -apply plugin: 'com.android.application' -apply plugin: 'com.google.gms.google-services' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 34 - ndkVersion flutter.ndkVersion + namespace = "com.example.prosapp_web_app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { - // coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.prosapp.prosapp" + applicationId = "com.example.prosapp_web_app" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion "21".toInteger() - targetSdkVersion 33 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName } 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.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation platform('com.google.firebase:firebase-bom:31.3.0') - implementation 'com.google.firebase:firebase-analytics-ktx' - //mplementation(platform("com.google.firebase:firebase-bom:32.2.2")) - //androidTestImplementation("com.google.firebase:firebase-appcheck-debug-testing") - // coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + source = "../.." } diff --git a/android/app/google-services.json b/android/app/google-services.json deleted file mode 100644 index c74eef4..0000000 --- a/android/app/google-services.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "project_info": { - "project_number": "245204384533", - "firebase_url": "https://prosapp-5747a-default-rtdb.firebaseio.com", - "project_id": "prosapp-5747a", - "storage_bucket": "prosapp-5747a.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:245204384533:android:e01772831d86f5c79f871c", - "android_client_info": { - "package_name": "com.prosapp.prosapp" - } - }, - "oauth_client": [ - { - "client_id": "245204384533-23bm14294ctm728nd9u83eiapsjfebhu.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.prosapp.prosapp", - "certificate_hash": "29ac5d7d1b88c755a084a0ad5d561dd62d795eb5" - } - }, - { - "client_id": "245204384533-4lo58sle0aclpq7jp4drepabs7v1tqe2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyBp7vsJvD6oXz0FORBLEeIELwId4a2onHQ" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "245204384533-4lo58sle0aclpq7jp4drepabs7v1tqe2.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.example.prosappco" - } - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index b1e34dd..399f698 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -45,4 +31,15 @@ android:name="flutterEmbedding" android:value="2" /> + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/prosapp_web_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/prosapp_web_app/MainActivity.kt new file mode 100644 index 0000000..eda6f12 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/prosapp_web_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.prosapp_web_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/kotlin/com/example/prosappco/MainActivity.kt b/android/app/src/main/kotlin/com/example/prosappco/MainActivity.kt deleted file mode 100644 index 1b7fd03..0000000 --- a/android/app/src/main/kotlin/com/example/prosappco/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.prosappco - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/android/app/src/main/kotlin/com/prosapp/prosappco/MainActivity.kt b/android/app/src/main/kotlin/com/prosapp/prosappco/MainActivity.kt deleted file mode 100644 index e91d64f..0000000 --- a/android/app/src/main/kotlin/com/prosapp/prosappco/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.prosapp.prosappco - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() { -} diff --git a/android/app/src/main/res/drawable-v21/bg_color.xml b/android/app/src/main/res/drawable-v21/bg_color.xml deleted file mode 100644 index a98be74..0000000 --- a/android/app/src/main/res/drawable-v21/bg_color.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index 01b3599..f74085f 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -1,12 +1,12 @@ - + - + diff --git a/android/app/src/main/res/drawable/bg_color.xml b/android/app/src/main/res/drawable/bg_color.xml deleted file mode 100644 index a98be74..0000000 --- a/android/app/src/main/res/drawable/bg_color.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 8dbf352..304732f 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -1,12 +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 index 0de4f5e..db77bb4 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/splash.png b/android/app/src/main/res/mipmap-hdpi/splash.png deleted file mode 100644 index 9ce5a18..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/splash.png and /dev/null 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 index 0de4f5e..17987b7 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/splash.png b/android/app/src/main/res/mipmap-mdpi/splash.png deleted file mode 100644 index 9ce5a18..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/splash.png and /dev/null 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 index 0de4f5e..09d4391 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/splash.png b/android/app/src/main/res/mipmap-xhdpi/splash.png deleted file mode 100644 index 9ce5a18..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/splash.png and /dev/null 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 index 0de4f5e..d5f1c8d 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/splash.png b/android/app/src/main/res/mipmap-xxhdpi/splash.png deleted file mode 100644 index 9ce5a18..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/splash.png and /dev/null 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 index 0de4f5e..4d6372e 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/splash.png b/android/app/src/main/res/mipmap-xxxhdpi/splash.png deleted file mode 100644 index 9ce5a18..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/values/color.xml b/android/app/src/main/res/values/color.xml deleted file mode 100644 index 0a9ee07..0000000 --- a/android/app/src/main/res/values/color.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #ffffff - \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index b1e34dd..399f698 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -15,14 +14,12 @@ - - - + - + @@ -31,10 +28,10 @@ - + - + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index a9cba04..f3c2851 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,10 +1,8 @@ - - - + + - - + @@ -16,14 +14,13 @@ - + - + - diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist deleted file mode 100644 index 5dc64a9..0000000 --- a/ios/Runner/GoogleService-Info.plist +++ /dev/null @@ -1,38 +0,0 @@ - - - - - CLIENT_ID - 245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn - ANDROID_CLIENT_ID - 245204384533-04oabs744hp8fjbdreijp4h16nvfmeti.apps.googleusercontent.com - API_KEY - AIzaSyDUhnkNkeEPkqcwVo5EWr63q3oEX6bDGic - GCM_SENDER_ID - 245204384533 - PLIST_VERSION - 1 - BUNDLE_ID - com.example.prosappco - PROJECT_ID - prosapp-5747a - STORAGE_BUCKET - prosapp-5747a.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:245204384533:ios:1d58f5624d0df4859f871c - DATABASE_URL - https://prosapp-5747a-default-rtdb.firebaseio.com - - \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 54ff67b..568842f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,22 +2,10 @@ - NSLocationWhenInUseUsageDescription - Nuestra aplicación necesita acceso a tu ubicación cuando la estás utilizando para proporcionarte recomendaciones de medicos cercanos. - NSLocationAlwaysAndWhenInUseUsageDescription - Nuestra aplicación necesita acceso a tu ubicación en todo momento y cuando está en uso para proporcionarte servicios de ubicación en tiempo real. - NSLocationUsageDescription - Esta aplicación necesita acceder a tu ubicación en todo momento para proporcionarte servicios de ubicación de los profesionales mas cercanos. - NSCameraUsageDescription - Esta aplicación necesita acceder a tu camara para modificar tu foto de perfil. - NSPhotoLibraryUsageDescription - Esta aplicación necesita acceder a tu biblioteca de fotos para modificar tu foto de perfil. - CADisableMinimumFrameDurationOnPhone - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Prosapp + Prosapp Web App CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -25,37 +13,17 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - prosappco + prosapp_web_app CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - app-1-245204384533-ios-1d58f5624d0df4859f871c - CFBundleURLSchemes - - app-1-245204384533-ios-1d58f5624d0df4859f871c - - - - CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - remote-notification - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -73,11 +41,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - - aps-environment - development - FirebaseAppDelegateProxyEnabled - NO + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements deleted file mode 100644 index 903def2..0000000 --- a/ios/Runner/RunnerDebug.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - aps-environment - development - - diff --git a/ios/Runner/RunnerProfile.entitlements b/ios/Runner/RunnerProfile.entitlements deleted file mode 100644 index 903def2..0000000 --- a/ios/Runner/RunnerProfile.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - aps-environment - development - - diff --git a/ios/Runner/RunnerRelease.entitlements b/ios/Runner/RunnerRelease.entitlements deleted file mode 100644 index 903def2..0000000 --- a/ios/Runner/RunnerRelease.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - aps-environment - development - - diff --git a/ios/firebase_app_id_file.json b/ios/firebase_app_id_file.json deleted file mode 100644 index 1c89e48..0000000 --- a/ios/firebase_app_id_file.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "file_generated_by": "FlutterFire CLI", - "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", - "GOOGLE_APP_ID": "1:245204384533:ios:1d58f5624d0df4859f871c", - "FIREBASE_PROJECT_ID": "prosapp-5747a", - "GCM_SENDER_ID": "245204384533" -} \ No newline at end of file diff --git a/lib/constansts.dart b/lib/constansts.dart deleted file mode 100644 index cc041a5..0000000 --- a/lib/constansts.dart +++ /dev/null @@ -1,76 +0,0 @@ -const String GOOGLE_MAPS_API_KEY = 'AIzaSyCW_og6qQ8W8G-5_BxIS4sBnl8cLkjL95s'; - -const String MAP_STYLE = ''' - [ - { - "elementType": "labels.icon", - "stylers": [ - { - "visibility": "on", - "color": "#6F6F6F" - } - ] - }, - { - "elementType": "labels.text.fill", - "stylers": [ - { - "color": "#616161" - } - ] - }, - { - "elementType": "labels.text.stroke", - "stylers": [ - { - "color": "#f5f5f5" - } - ] - }, - { - "featureType": "administrative.land_parcel", - "elementType": "labels.text.fill", - "stylers": [ - { - "color": "#bdbdbd" - } - ] - }, - { - "featureType": "poi", - "elementType": "geometry", - "stylers": [ - { - "color": "#eeeeee" - } - ] - }, - { - "featureType": "poi", - "elementType": "labels.text.fill", - "stylers": [ - { - "color": "#757575" - } - ] - }, - { - "featureType": "poi.park", - "elementType": "geometry", - "stylers": [ - { - "color": "#e5e5e5" - } - ] - }, - { - "featureType": "poi.park", - "elementType": "labels.text.fill", - "stylers": [ - { - "color": "#9e9e9e" - } - ] - } -] - '''; diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart deleted file mode 100644 index 9234cf8..0000000 --- a/lib/firebase_options.dart +++ /dev/null @@ -1,2 +0,0 @@ -// ponytail: Firebase removed; this file is kept as an empty stub so any lingering references compile. -// No imports, no exports needed. diff --git a/lib/main.dart b/lib/main.dart index 66aa459..629177f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,46 +1,81 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/presentation/screens/service_web.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:prosappco/src/presentation/screens/calendar.dart'; -import 'package:prosappco/src/presentation/screens/city.dart'; -import 'package:prosappco/src/presentation/screens/login/login.dart'; -import 'package:prosappco/src/presentation/screens/login/login_email.dart'; -import 'package:prosappco/src/presentation/screens/my_services.dart'; -import 'package:prosappco/src/presentation/screens/my_services_pro.dart'; -import 'package:prosappco/src/presentation/screens/new_number.dart'; -import 'package:prosappco/src/presentation/screens/new_password.dart'; -import 'package:prosappco/src/presentation/screens/profession.dart'; -import 'package:prosappco/src/presentation/screens/professional_profile.dart'; -import 'package:prosappco/src/presentation/screens/professional_revision.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile_pro.dart'; -import 'package:prosappco/src/presentation/screens/register/register.dart'; -import 'package:prosappco/src/presentation/screens/request_sent.dart'; -import 'package:prosappco/src/presentation/screens/reset_password/reset_password.dart'; -import 'package:prosappco/src/presentation/screens/map/service.dart'; -import 'package:prosappco/src/presentation/screens/solicitudes.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/calendar_services_provider.dart'; +import 'package:prosapp_web_app/providers/chat_provider.dart'; +import 'package:prosapp_web_app/providers/cities_provider.dart'; +import 'package:prosapp_web_app/providers/professional_detail_provider.dart'; +import 'package:prosapp_web_app/providers/professional_form_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/professionals_provider.dart'; +import 'package:prosapp_web_app/providers/professions_provider.dart'; +import 'package:prosapp_web_app/providers/profile_form_provider.dart'; +import 'package:prosapp_web_app/providers/score_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/providers/settings_provider.dart'; +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/local_storage.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/layouts/auth/auth_layout.dart'; +import 'package:prosapp_web_app/ui/layouts/dashboard/dashboard_layout.dart'; +import 'package:prosapp_web_app/ui/layouts/splash/splash_layout.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Trigger singleton init which schedules _checkSession after first frame - AuthenticationRepository.instance; + await dotenv.load(fileName: ".env"); - await initializeDateFormatting('es_MX', null); + await Firebase.initializeApp( + options: FirebaseOptions( + apiKey: dotenv.env['API_KEY']!, + authDomain: dotenv.env['AUTH_DOMAIN'], + projectId: dotenv.env['PROJECT_ID']!, + storageBucket: dotenv.env['STORAGE_BUCKET'], + messagingSenderId: dotenv.env['MESSAGING_SENDER_ID']!, + appId: dotenv.env['APP_ID']!, + measurementId: dotenv.env['MEASUREMENT_ID'], + ), + ); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); + await LocalStorage.configurePrefs(); - runApp(const MyApp()); + await initializeDateFormatting('es_ES', null); + + Flurorouter.configureRoutes(); + + runApp(const AppState()); +} + +class AppState extends StatelessWidget { + const AppState({super.key}); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(lazy: false, create: (_) => AuthProvider()), + ChangeNotifierProvider(lazy: false, create: (_) => SideMenuProvider()), + ChangeNotifierProvider(lazy: false, create: (_) => ProfessionalProvider()), + ChangeNotifierProvider(create: (_) => SettingsProvider()), + ChangeNotifierProvider(create: (_) => ProfileFormProvider()), + ChangeNotifierProvider(create: (_) => ProfessionalFormProvider()), + ChangeNotifierProvider(create: (_) => ProfessionalsProvider()), + ChangeNotifierProvider(create: (_) => ServicesProvider()), + ChangeNotifierProvider(create: (_) => CalendarServicesProvider()), + ChangeNotifierProvider(create: (_) => CitiesProvider()), + ChangeNotifierProvider(create: (_) => ProfessionsProvider()), + ChangeNotifierProvider(create: (_) => ProfessionalDetailProvider()), + ChangeNotifierProvider(create: (_) => ScoreProvider()), + ChangeNotifierProvider(create: (_) => ChatProvider()), + ], + child: const MyApp(), + ); + } } class MyApp extends StatelessWidget { @@ -48,79 +83,32 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => UserProvider()), - ], - child: MaterialApp( - navigatorKey: appNavigatorKey, - theme: ThemeData(fontFamily: 'Poppins'), - debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('es', 'US')], - title: 'ProsApp', - initialRoute: '/', - routes: { - '/': (context) => const LoginScreen(), - '/splash': (context) => const SplashScreen(), - '/login': (context) => const LoginEmailScreen(), - '/profile': (context) => const ProfileScreen(), - '/register': (context) => const RegisterScreen(), - '/city': (context) => const CityScreen(), - '/profession': (context) => const ProfessionScreen(), - '/newNumber': (context) => const NewNumberScreen(), - '/profesionalRevision': (context) => const ProfessionalRevisionScreen(), - '/profesionalProfile': (context) => const ProfessionalProfileScreen(), - '/solicitudEnviada': (context) => const RequestSentScreen(), - '/nuevaPassword': (context) => NewPasswordScreen(), - '/servicio': (context) => const ServiceScreen(), - '/servicioWeb': (context) => const ServiceWebScreen(), - '/profilePro': (context) => const ProfileProScreen(), - '/calendar': (context) => const CalendarScreen(), - '/solicitud': (context) => SolicitudScreen(), - '/misserviciospro': (context) => MyServicesProScreen(), - '/misservicios': (context) => MyServicesScreen(), - '/resetpassword': (context) => const ResetPasswordScreen(), - }, - onGenerateRoute: (settings) { - throw Exception('Ruta desconocida: ${settings.name}'); - }, - ), - ); - } -} + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'ProsApp', + initialRoute: '/', + onGenerateRoute: Flurorouter.router.generator, + navigatorKey: NavigationService.navigatorKey, + scaffoldMessengerKey: NotificationsService.messengerKey, + builder: (_, child) { + final authProvider = Provider.of(context); -class SplashScreen extends StatefulWidget { - const SplashScreen({super.key}); + if (authProvider.authStatus == AuthStatus.checking) { + return const SplashLayout(); + } - @override - State createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State { - @override - void initState() { - super.initState(); - if (kIsWeb) { - Future.delayed(const Duration(seconds: 3), () { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const LoginScreen()), - ); - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.blue, - body: Center( - child: kIsWeb ? Image.asset('/images/splash.png') : const LoginScreen(), + if (authProvider.authStatus == AuthStatus.authenticated) { + return DashboardLayout(child: child!); + } else { + return AuthLayout(child: child!); + } + }, + theme: ThemeData.light().copyWith( + scrollbarTheme: const ScrollbarThemeData().copyWith( + thumbColor: WidgetStateProperty.all( + Colors.grey.withOpacity(0.5), + ), + ), ), ); } diff --git a/lib/models/chat_entity.dart b/lib/models/chat_entity.dart new file mode 100644 index 0000000..5c04623 --- /dev/null +++ b/lib/models/chat_entity.dart @@ -0,0 +1,45 @@ +import 'package:prosapp_web_app/models/message_entity.dart'; + +class ChatEntity { + final String? id; + final String userId; + final String professionalId; + final List messages; + + const ChatEntity({ + required this.id, + required this.userId, + required this.professionalId, + required this.messages, + }); + + static ChatEntity fromDocument(Map doc) { + return ChatEntity( + id: doc['id'] as String, + userId: doc['user_id'] as String, + professionalId: doc['professional_id'] as String, + messages: (doc['messages'] as List) + .map((e) => MessageEntity.fromDocument(e as Map)) + .toList(), + ); + } + + Map toDocument() { + return { + 'id': id, + 'user_id': userId, + 'professional_id': professionalId, + 'messages': messages.map((e) => e.toDocument()).toList(), + }; + } + + @override + String toString() { + return '''ChatEntity{ + id: $id, + userId: $userId, + professionalId: $professionalId, + messages: $messages + }'''; + } +} diff --git a/lib/models/city.dart b/lib/models/city.dart new file mode 100644 index 0000000..8eb1cc9 --- /dev/null +++ b/lib/models/city.dart @@ -0,0 +1,13 @@ +class City { + final String cityName; + final String coordsOfCity; + final String stateOfCity; + final String countryOfCity; + + const City({ + required this.cityName, + required this.coordsOfCity, + required this.stateOfCity, + required this.countryOfCity, + }); +} diff --git a/lib/models/city_entity.dart b/lib/models/city_entity.dart new file mode 100644 index 0000000..e70e5c2 --- /dev/null +++ b/lib/models/city_entity.dart @@ -0,0 +1,31 @@ +class CityEntity { + final String name; + final String coords; + + const CityEntity({ + required this.name, + required this.coords, + }); + + Map toDocument() { + return { + 'name': name, + 'coords': coords, + }; + } + + static CityEntity fromDocument(Map doc) { + return CityEntity( + name: doc['name'] as String, + coords: doc['coords'] as String, + ); + } + + @override + String toString() { + return '''CityEntity { + name: $name + coords: $coords + }'''; + } +} diff --git a/lib/models/comment_entity.dart b/lib/models/comment_entity.dart new file mode 100644 index 0000000..a9ea835 --- /dev/null +++ b/lib/models/comment_entity.dart @@ -0,0 +1,58 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class CommentEntity { + final String authorId; + final String destinationId; + final String serviceId; + final String content; + final double score; + final bool isFromUser; + final Timestamp createdAt; + + const CommentEntity({ + required this.authorId, + required this.destinationId, + required this.serviceId, + required this.content, + required this.score, + required this.isFromUser, + required this.createdAt, + }); + + static CommentEntity fromDocument(Map doc) { + return CommentEntity( + authorId: doc['author_id'] as String, + destinationId: doc['destination_id'] as String, + serviceId: doc['service_id'] as String, + content: doc['content'] as String, + score: doc['score'] as double, + isFromUser: doc['is_from_user'] as bool, + createdAt: doc['created_at'] as Timestamp, + ); + } + + Map toDocument() { + return { + 'author_id': authorId, + 'destination_id': destinationId, + 'service_id': serviceId, + 'content': content, + 'score': score, + 'is_from_user': isFromUser, + 'created_at': createdAt, + }; + } + + @override + String toString() { + return '''CommentEntity{ + authorId: $authorId, + destinationId: $destinationId, + serviceId: $serviceId, + content: $content, + score: $score, + isFromUser: $isFromUser, + createdAt: $createdAt + }'''; + } +} diff --git a/lib/models/country_entity.dart b/lib/models/country_entity.dart new file mode 100644 index 0000000..7c754ae --- /dev/null +++ b/lib/models/country_entity.dart @@ -0,0 +1,35 @@ +import 'package:prosapp_web_app/models/region_entity.dart'; + +class CountryEntity { + final String name; + final List regions; + + const CountryEntity({ + required this.name, + required this.regions, + }); + + Map toDocument() { + return { + 'name': name, + 'states': regions, + }; + } + + static CountryEntity fromDocument(Map doc) { + return CountryEntity( + name: doc['name'] as String, + regions: (doc['states'] as List) + .map((region) => RegionEntity.fromDocument(region)) + .toList(), + ); + } + + @override + String toString() { + return '''CountryEntity { + name: $name + regions: $regions + }'''; + } +} diff --git a/lib/models/location_preferences.dart b/lib/models/location_preferences.dart new file mode 100644 index 0000000..7329985 --- /dev/null +++ b/lib/models/location_preferences.dart @@ -0,0 +1,11 @@ +export 'location_preferences.dart'; + +enum LocationPreferences { office, delivery, both } + + int enumToInt(LocationPreferences state) { + return state.index; +} + +LocationPreferences intToEnum(int value) { + return LocationPreferences.values[value]; +} diff --git a/lib/models/message_entity.dart b/lib/models/message_entity.dart new file mode 100644 index 0000000..eff2a56 --- /dev/null +++ b/lib/models/message_entity.dart @@ -0,0 +1,36 @@ +class MessageEntity { + final String ownerId; + final String content; + final DateTime createdAt; + + const MessageEntity({ + required this.ownerId, + required this.content, + required this.createdAt, + }); + + static MessageEntity fromDocument(Map doc) { + return MessageEntity( + ownerId: doc['owner_id'] as String, + content: doc['content'] as String, + createdAt: DateTime.parse(doc['created_at'] as String), + ); + } + + Map toDocument() { + return { + 'owner_id': ownerId, + 'content': content, + 'created_at': createdAt.toIso8601String(), + }; + } + + @override + String toString() { + return '''MessageEntity{ + ownerId: $ownerId, + content: $content, + createdAt: $createdAt + }'''; + } +} diff --git a/lib/models/payment_method_entity.dart b/lib/models/payment_method_entity.dart new file mode 100644 index 0000000..4a6222a --- /dev/null +++ b/lib/models/payment_method_entity.dart @@ -0,0 +1,54 @@ +class PaymentMethodEntity { + final bool nequi; + final bool datafono; + final bool transferencia; + + static const empty = PaymentMethodEntity( + nequi: false, + datafono: false, + transferencia: false, + ); + + const PaymentMethodEntity({ + required this.nequi, + required this.datafono, + required this.transferencia, + }); + + PaymentMethodEntity copyWith({ + bool? datafono, + bool? nequi, + bool? transferencia, + }) { + return PaymentMethodEntity( + nequi: nequi ?? this.nequi, + datafono: datafono ?? this.datafono, + transferencia: transferencia ?? this.transferencia, + ); + } + + Map toDocument() { + return { + 'nequi': nequi, + 'datafono': datafono, + 'transferencia': transferencia, + }; + } + + static PaymentMethodEntity fromDocument(Map doc) { + return PaymentMethodEntity( + nequi: doc['nequi'] as bool, + datafono: doc['datafono'] as bool, + transferencia: doc['transferencia'] as bool, + ); + } + + @override + String toString() { + return '''PaymentMethodEntity { + nequi: $nequi + datafono: $datafono + transferencia: $transferencia + }'''; + } +} diff --git a/lib/models/pro_state.dart b/lib/models/pro_state.dart new file mode 100644 index 0000000..ceea42b --- /dev/null +++ b/lib/models/pro_state.dart @@ -0,0 +1,9 @@ +enum ProState { inactive, pending, active, denied } + +int enumToInt(ProState state) { + return state.index; +} + +ProState intToEnum(int value) { + return ProState.values[value]; +} diff --git a/lib/models/profesional.dart b/lib/models/profesional.dart new file mode 100644 index 0000000..ac73f01 --- /dev/null +++ b/lib/models/profesional.dart @@ -0,0 +1,153 @@ +import 'package:prosapp_web_app/models/location_preferences.dart'; +import 'package:prosapp_web_app/models/payment_method_entity.dart'; +import 'package:prosapp_web_app/models/schedules.dart'; + +class Profesional { + final String id; + final String identification; + final String address; + final String aditionalAddress; + final String profession; + final bool ratePreferences; + final String rate; + final LocationPreferences locationPreferences; + final String bannerPicture; + final String identificationPicture; + final String certificatePicture; + final double latitude; + final double longitude; + final List specializations; + final List specializationsPictures; + final Schedules schedules; + final PaymentMethodEntity paymentMethods; + + const Profesional({ + required this.id, + required this.identification, + required this.address, + required this.aditionalAddress, + required this.profession, + required this.ratePreferences, + required this.rate, + required this.locationPreferences, + required this.bannerPicture, + required this.identificationPicture, + required this.certificatePicture, + required this.latitude, + required this.longitude, + required this.specializations, + required this.specializationsPictures, + required this.schedules, + required this.paymentMethods, + }); + + static Profesional fromDocument(Map doc) { + return Profesional( + id: doc['id'] as String, + identification: doc['identification'] as String, + address: doc['address'] as String, + aditionalAddress: doc['aditional_address'] as String, + profession: doc['profession'] as String, + ratePreferences: doc['rate_preferences'] as bool, + rate: doc['rate'] as String, + locationPreferences: intToEnum(doc['location_preferences'] as int), + bannerPicture: doc['banner_picture'] as String, + identificationPicture: doc['identification_picture'] as String, + certificatePicture: doc['certificate_picture'] as String, + latitude: double.parse(doc['latitude'].toString()), + longitude: double.parse(doc['longitude'].toString()), + specializations: List.from(doc['specializations']), + specializationsPictures: + List.from(doc['specializations_pictures']), + schedules: Schedules.fromDocument(doc['schedules']), + paymentMethods: PaymentMethodEntity.fromDocument(doc['payment_methods']), + ); + } + + Profesional copyWith({ + String? id, + String? identification, + String? address, + String? aditionalAddress, + String? profession, + bool ratePreferences = false, + String? rate, + LocationPreferences? locationPreferences, + String? bannerPicture, + String? identificationPicture, + String? certificatePicture, + double? latitude, + double? longitude, + List? specializations, + List? specializationsPictures, + Schedules? schedules, + PaymentMethodEntity? paymentMethods, + }) { + return Profesional( + id: id ?? this.id, + identification: identification ?? this.identification, + address: address ?? this.address, + aditionalAddress: aditionalAddress ?? this.aditionalAddress, + profession: profession ?? this.profession, + ratePreferences: ratePreferences, + rate: rate ?? this.rate, + locationPreferences: locationPreferences ?? this.locationPreferences, + bannerPicture: bannerPicture ?? this.bannerPicture, + identificationPicture: + identificationPicture ?? this.identificationPicture, + certificatePicture: certificatePicture ?? this.certificatePicture, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + specializations: specializations ?? this.specializations, + specializationsPictures: + specializationsPictures ?? this.specializationsPictures, + schedules: schedules ?? this.schedules, + paymentMethods: paymentMethods ?? this.paymentMethods, + ); + } + + Map toDocument() { + return { + 'id': id, + 'identification': identification, + 'address': address, + 'aditional_address': aditionalAddress, + 'profession': profession, + 'rate_preferences': ratePreferences, + 'rate': rate, + 'location_preferences': enumToInt(locationPreferences), + 'banner_picture': bannerPicture, + 'identification_picture': identificationPicture, + 'certificate_picture': certificatePicture, + 'latitude': latitude, + 'longitude': longitude, + 'specializations': specializations, + 'specializations_pictures': specializationsPictures, + 'schedules': schedules.toJson(), + 'payment_methods': paymentMethods.toDocument(), + }; + } + + @override + String toString() { + return '''Profesional{ { + id: $id, + identification: $identification, + address: $address, + aditional_address: $aditionalAddress, + profession: $profession, + rate_preferences: $ratePreferences, + rate: $rate, + location_preferences: $locationPreferences, + bannerPicture: $bannerPicture, + identificationPicture: $identificationPicture, + certificatePicture: $certificatePicture, + latitude: $latitude, + longitude: $longitude, + specializations: $specializations, + specializationsPictures: $specializationsPictures, + schedule: $schedules, + paymentMethods: $paymentMethods + }'''; + } +} diff --git a/lib/models/profession.dart b/lib/models/profession.dart new file mode 100644 index 0000000..a2091c1 --- /dev/null +++ b/lib/models/profession.dart @@ -0,0 +1,22 @@ +class Profession { + final String name; + + Profession({required this.name}); + + Map toDocument() { + return { + 'name': name, + }; + } + + static Profession fromDocument(Map doc) { + return Profession( + name: doc['name'] as String, + ); + } + + @override + String toString() { + return 'Profession{name: $name}'; + } +} diff --git a/lib/models/region_entity.dart b/lib/models/region_entity.dart new file mode 100644 index 0000000..22c3033 --- /dev/null +++ b/lib/models/region_entity.dart @@ -0,0 +1,36 @@ + +import 'package:prosapp_web_app/models/city_entity.dart'; + +class RegionEntity { + final String name; + final List cities; + + const RegionEntity({ + required this.name, + required this.cities, + }); + + Map toDocument() { + return { + 'name': name, + 'cities': cities, + }; + } + + static RegionEntity fromDocument(Map doc) { + return RegionEntity( + name: doc['name'] as String, + cities: (doc['cities'] as List) + .map((city) => CityEntity.fromDocument(city)) + .toList(), + ); + } + + @override + String toString() { + return '''RegionEntity { + name: $name + cities: $cities + }'''; + } +} diff --git a/lib/models/schedules.dart b/lib/models/schedules.dart new file mode 100644 index 0000000..05dacd7 --- /dev/null +++ b/lib/models/schedules.dart @@ -0,0 +1,76 @@ +import 'package:prosapp_web_app/models/schedules_entity.dart'; + +class Schedules { + final ScheduleEntity monday; + final ScheduleEntity tuesday; + final ScheduleEntity wednesday; + final ScheduleEntity thursday; + final ScheduleEntity friday; + final ScheduleEntity saturday; + final ScheduleEntity sunday; + + const Schedules({ + required this.monday, + required this.tuesday, + required this.wednesday, + required this.thursday, + required this.friday, + required this.saturday, + required this.sunday, + }); + + static const empty = Schedules( + monday: ScheduleEntity.empty, + tuesday: ScheduleEntity.empty, + wednesday: ScheduleEntity.empty, + thursday: ScheduleEntity.empty, + friday: ScheduleEntity.empty, + saturday: ScheduleEntity.empty, + sunday: ScheduleEntity.empty, + ); + + // Copy function + Schedules copyWith({ + ScheduleEntity? monday, + ScheduleEntity? tuesday, + ScheduleEntity? wednesday, + ScheduleEntity? thursday, + ScheduleEntity? friday, + ScheduleEntity? saturday, + ScheduleEntity? sunday, + }) { + return Schedules( + monday: monday ?? this.monday, + tuesday: tuesday ?? this.tuesday, + wednesday: wednesday ?? this.wednesday, + thursday: thursday ?? this.thursday, + friday: friday ?? this.friday, + saturday: saturday ?? this.saturday, + sunday: sunday ?? this.sunday, + ); + } + + factory Schedules.fromDocument(Map doc) { + return Schedules( + monday: ScheduleEntity.fromDocument(doc['monday']), + tuesday: ScheduleEntity.fromDocument(doc['tuesday']), + wednesday: ScheduleEntity.fromDocument(doc['wednesday']), + thursday: ScheduleEntity.fromDocument(doc['thursday']), + friday: ScheduleEntity.fromDocument(doc['friday']), + saturday: ScheduleEntity.fromDocument(doc['saturday']), + sunday: ScheduleEntity.fromDocument(doc['sunday']), + ); + } + + Map toJson() { + return { + 'monday': monday.toJson(), + 'tuesday': tuesday.toJson(), + 'wednesday': wednesday.toJson(), + 'thursday': thursday.toJson(), + 'friday': friday.toJson(), + 'saturday': saturday.toJson(), + 'sunday': sunday.toJson(), + }; + } +} diff --git a/lib/models/schedules_entity.dart b/lib/models/schedules_entity.dart new file mode 100644 index 0000000..16d1caa --- /dev/null +++ b/lib/models/schedules_entity.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class ScheduleEntity { + final bool enabled; + final bool continuousDay; + final TimeOfDay? range1Hour1; + final TimeOfDay? range1Hour2; + final TimeOfDay? range2Hour1; + final TimeOfDay? range2Hour2; + + const ScheduleEntity({ + required this.enabled, + required this.continuousDay, + required this.range1Hour1, + required this.range1Hour2, + required this.range2Hour1, + required this.range2Hour2, + }); + + static const empty = ScheduleEntity( + enabled: false, + continuousDay: false, + range1Hour1: null, + range1Hour2: null, + range2Hour1: null, + range2Hour2: null, + ); + + ScheduleEntity copyWith({ + bool? enabled, + bool? continuousDay, + TimeOfDay? range1Hour1, + TimeOfDay? range1Hour2, + TimeOfDay? range2Hour1, + TimeOfDay? range2Hour2, + }) { + return ScheduleEntity( + enabled: enabled ?? this.enabled, + continuousDay: continuousDay ?? this.continuousDay, + range1Hour1: range1Hour1 ?? this.range1Hour1, + range1Hour2: range1Hour2 ?? this.range1Hour2, + range2Hour1: range2Hour1 ?? this.range2Hour1, + range2Hour2: range2Hour2 ?? this.range2Hour2, + ); + } + + static ScheduleEntity fromDocument(Map doc) { + return ScheduleEntity( + enabled: doc['habilitado'] as bool, + continuousDay: doc['continuous_day'] as bool, + range1Hour1: _parseTime(doc['range1Hour1']), + range1Hour2: _parseTime(doc['range1Hour2']), + range2Hour1: _parseTime(doc['range2Hour1']), + range2Hour2: _parseTime(doc['range2Hour2']), + ); + } + + static TimeOfDay? _parseTime(String? time) { + try { + if (time == null) return null; + final components = time.split(':'); + if (components.length != 2) { + return null; + } + final hour = int.parse(components[0]); + final minutes = int.parse(components[1]); + return TimeOfDay(hour: hour, minute: minutes); + } catch (e) { + return null; + } + } + + Map toJson() { + return { + 'habilitado': enabled, + 'continuous_day': continuousDay, + 'range1Hour1': formatTimeOfDay(range1Hour1), + 'range1Hour2': formatTimeOfDay(range1Hour2), + 'range2Hour1': formatTimeOfDay(range2Hour1), + 'range2Hour2': formatTimeOfDay(range2Hour2), + }; + } + + String? formatTimeOfDay(TimeOfDay? time) { + if (time != null) { + return "${time.hour.toString()}:${time.minute.toString()}"; + } + return null; + } + + static String? getFormatTime(TimeOfDay? time) { + if (time == null) { + return null; + } + final now = DateTime.now(); + final dateTime = + DateTime(now.year, now.month, now.day, time.hour, time.minute); + final format = DateFormat.jm(); + return format.format(dateTime); + } + + @override + String toString() { + return '''ScheduleEntity { + enabled: $enabled, + continuousDay: $continuousDay, + range1Hour1: $range1Hour1, + range1Hour2: $range1Hour2, + range2Hour1: $range2Hour1, + range2Hour2: $range2Hour2 + }'''; + } +} diff --git a/lib/models/score_entity.dart b/lib/models/score_entity.dart new file mode 100644 index 0000000..bb24e26 --- /dev/null +++ b/lib/models/score_entity.dart @@ -0,0 +1,36 @@ +class ReputationEntity { + final int total; + final double average; + final int totalPro; + final double averagePro; + + const ReputationEntity({ + required this.total, + required this.average, + required this.totalPro, + required this.averagePro, + }); + + static ReputationEntity fromDocument(Map doc) { + final total = doc['total'] ?? 0; + final totalPro = doc['total_pro'] ?? 0; + final average = doc['average'] ?? 0.0; + final averagePro = doc['average_pro'] ?? 0.0; + + return ReputationEntity( + total: int.parse(total.toString()), + average: double.parse(average.toString()), + totalPro: int.parse(totalPro.toString()), + averagePro: double.parse(averagePro.toString()), + ); + } + + Map toDocument() { + return { + 'total': total, + 'average': average, + 'total_pro': totalPro, + 'average_pro': averagePro, + }; + } +} diff --git a/lib/models/service.dart b/lib/models/service.dart new file mode 100644 index 0000000..0614a0d --- /dev/null +++ b/lib/models/service.dart @@ -0,0 +1,116 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/service_location_preferences.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; + +class Service { + final String? id; + final String professionalId; + final bool professionalScored; + final String userId; + final bool userScored; + final String address; + final String aditionalAddress; + final double latitude; + final double longitude; + final String day; + final Timestamp createdAt; + final String description; + final TimeOfDay range1Hour1; + final TimeOfDay range1Hour2; + final String rate; + final ServiceStatus status; + final ServiceLocationPreferences location; + + Service({ + required this.id, + required this.professionalId, + required this.professionalScored, + required this.userId, + required this.userScored, + required this.address, + required this.aditionalAddress, + required this.latitude, + required this.longitude, + required this.day, + required this.createdAt, + required this.description, + required this.range1Hour1, + required this.range1Hour2, + required this.rate, + required this.status, + required this.location, + }); + + static Service fromJson(Map doc, String id) { + return Service( + id: id, + professionalId: doc['professional_id'] as String, + professionalScored: doc['professional_scored'] as bool, + userId: doc['user_id'] as String, + userScored: doc['user_scored'] as bool, + address: doc['address'] as String, + aditionalAddress: doc['aditional_address'] as String, + latitude: doc['latitude'] as double, + longitude: doc['longitude'] as double, + day: doc['day'] as String, + createdAt: doc['created_at'] as Timestamp, + description: doc['description'] as String, + range1Hour1: parseTimeOfDay(doc['range1_hour1'] as String), + range1Hour2: parseTimeOfDay(doc['range1_hour2'] as String), + status: intToEnumService(doc['status'] as int), + rate: doc['rate'] as String, + location: intToEnum(doc['location'] as int), + ); + } + + static Service fromDocument(DocumentSnapshot> doc) { + return fromJson(doc.data()!, doc.id); + } + + Map toDocument() { + return { + 'id': id, + 'professional_id': professionalId, + 'professional_scored': professionalScored, + 'user_id': userId, + 'user_scored': userScored, + 'address': address, + 'aditional_address': aditionalAddress, + 'latitude': latitude, + 'longitude': longitude, + 'day': day, + 'created_at': createdAt, + 'description': description, + 'range1_hour1': formatTimeOfDay(range1Hour1), + 'range1_hour2': formatTimeOfDay(range1Hour2), + 'rate': rate, + 'status': enumToIntService(status), + 'location': enumToInt(location), + }; + } + + static TimeOfDay parseTimeOfDay(String timeString) { + final parts = timeString.split(':'); + return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1])); + } + + String? formatTimeOfDay(TimeOfDay? time) { + if (time != null) { + return "${time.hour.toString()}:${time.minute.toString()}"; + } + return null; + } + + @override + String toString() { + return '''Service { + professionalId: $professionalId, + userId: $userId, + day: $day, + createdAt: $createdAt, + status: $status, + location: $location + }'''; + } +} diff --git a/lib/models/service_location_preferences.dart b/lib/models/service_location_preferences.dart new file mode 100644 index 0000000..912fdb8 --- /dev/null +++ b/lib/models/service_location_preferences.dart @@ -0,0 +1,11 @@ +export 'service_location_preferences.dart'; + +enum ServiceLocationPreferences { office, delivery } + +int enumToInt(ServiceLocationPreferences state) { + return state.index; +} + +ServiceLocationPreferences intToEnum(int value) { + return ServiceLocationPreferences.values[value]; +} diff --git a/lib/models/service_status.dart b/lib/models/service_status.dart new file mode 100644 index 0000000..73b13cc --- /dev/null +++ b/lib/models/service_status.dart @@ -0,0 +1,19 @@ +export 'service_status.dart'; + +enum ServiceStatus { + pending, // 0 + acepted, // 1 + denied, // 2 + active, // 3 + cancelled, // 4 + completed, // 5 + selfBooked // 6 +} + +int enumToIntService(ServiceStatus state) { + return state.index; +} + +ServiceStatus intToEnumService(int value) { + return ServiceStatus.values[value]; +} diff --git a/lib/models/servicio_profesional.dart b/lib/models/servicio_profesional.dart new file mode 100644 index 0000000..0314cf3 --- /dev/null +++ b/lib/models/servicio_profesional.dart @@ -0,0 +1,20 @@ +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; + +class ServicioProfesional { + final Service service; + final Usuario user; + + ServicioProfesional({ + required this.user, + required this.service, + }); + + @override + String toString() { + return '''UserProfessional{ + myUser: $user, + service: $service + }'''; + } +} diff --git a/lib/models/setting.dart b/lib/models/setting.dart new file mode 100644 index 0000000..26f7e87 --- /dev/null +++ b/lib/models/setting.dart @@ -0,0 +1,88 @@ +class Setting { + final bool tarifas; + final bool domicilios; + final bool google; + final String versionIos; + final String versionAndroid; + final String horaNotificacion; + final String tituloSoporte; + final String parrafoSoporte; + final String numeroSoporte; + final String emailSoporte; + final String diasSoporte; + final String horasSoporte; + final String politicasPrivacidad; + final String politicasPrivacidadTitle; + final String politicasPrivacidadBody; + final String terminosCondiciones; + final String terminosCondicionesTitle; + final String terminosCondicionesBody; + + const Setting({ + required this.tarifas, + required this.domicilios, + required this.google, + required this.versionIos, + required this.versionAndroid, + required this.horaNotificacion, + required this.tituloSoporte, + required this.parrafoSoporte, + required this.numeroSoporte, + required this.emailSoporte, + required this.diasSoporte, + required this.horasSoporte, + required this.politicasPrivacidad, + required this.politicasPrivacidadTitle, + required this.politicasPrivacidadBody, + required this.terminosCondiciones, + required this.terminosCondicionesTitle, + required this.terminosCondicionesBody, + }); + + static Setting fromDocument(Map doc) { + return Setting( + tarifas: doc['tarifas'] as bool, + domicilios: doc['domicilios'] as bool, + google: doc['google'] as bool, + versionIos: doc['version_ios'] as String, + versionAndroid: doc['version_android'] as String, + horaNotificacion: doc['hora_notificacion'] as String, + tituloSoporte: doc['titulo_soporte'] as String, + parrafoSoporte: doc['parrafo_soporte'] as String, + numeroSoporte: doc['numero_soporte'] as String, + emailSoporte: doc['email_soporte'] as String, + diasSoporte: doc['dias_soporte'] as String, + horasSoporte: doc['horas_soporte'] as String, + politicasPrivacidad: doc['politicas_privacidad'] as String, + politicasPrivacidadTitle: doc['politicas_privacidad_title'] as String, + politicasPrivacidadBody: doc['politicas_privacidad_body'] as String, + terminosCondiciones: doc['terminos_condiciones'] as String, + terminosCondicionesTitle: doc['terminos_condiciones_title'] as String, + terminosCondicionesBody: doc['terminos_condiciones_body'] as String, + ); + } + + @override + String toString() { + return '''Setting { + tarifas: $tarifas + domicilios: $domicilios + google: $google + versionIos: $versionIos + versionAndroid: $versionAndroid + horaNotificacion: $horaNotificacion + tituloSoporte: $tituloSoporte + parrafoSoporte: $parrafoSoporte + numeroSoporte: $numeroSoporte + emailSoporte: $emailSoporte + diasSoporte: $diasSoporte + horasSoporte: $horasSoporte + politicasPrivacidad: $politicasPrivacidad + politicasPrivacidadTitle: $politicasPrivacidadTitle + politicasPrivacidadBody: $politicasPrivacidadBody + terminosCondiciones: $terminosCondiciones + terminosCondicionesTitle: $terminosCondicionesTitle + terminosCondicionesBody: $terminosCondicionesBody + }'''; + } +} diff --git a/lib/models/usuario.dart b/lib/models/usuario.dart new file mode 100644 index 0000000..a22cc30 --- /dev/null +++ b/lib/models/usuario.dart @@ -0,0 +1,66 @@ +import 'package:prosapp_web_app/models/pro_state.dart'; + +class Usuario { + final String id; + final String? email; + final String? phone; + final String name; + final String? nickname; + final String? city; + final String? picture; + final String? birthday; + final String? gender; + final ProState proState; + final String? token; + + Usuario({ + required this.id, + required this.email, + required this.phone, + required this.name, + required this.nickname, + required this.city, + required this.picture, + required this.birthday, + required this.gender, + required this.proState, + required this.token, + }); + + Map toDocument() { + return { + 'id': id, + 'email': email, + 'phone': phone, + 'name': name, + 'nickname': name.toLowerCase().trim().replaceAll(' ', '_'), + 'city': city, + 'picture': picture, + 'birthday': birthday, + 'gender': gender, + 'professional_state': enumToInt(proState), + 'token': token, + }; + } + + static Usuario fromDocument(Map doc) { + return Usuario( + id: doc['id'], + email: doc['email'], + phone: doc['phone'], + name: doc['name'] ?? '', + nickname: doc['nickname'], + city: doc['city'], + picture: doc['picture'], + birthday: doc['birthday'], + gender: doc['gender'], + proState: intToEnum(doc['professional_state'] as int), + token: doc['token'], + ); + } + + @override + String toString() { + return 'User(id: $id, email: $email, phone: $phone, name: $name, nickname: $nickname, city: $city, picture: $picture, birthday: $birthday, gender: $gender, proState: $proState, token: $token)'; + } +} diff --git a/lib/models/usuario_profesional.dart b/lib/models/usuario_profesional.dart new file mode 100644 index 0000000..1e61f76 --- /dev/null +++ b/lib/models/usuario_profesional.dart @@ -0,0 +1,41 @@ +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; + +class UsuarioProfesional { + final Usuario user; + final Profesional professionalInfo; + final double averageScore; + + UsuarioProfesional({ + required this.user, + required this.professionalInfo, + required this.averageScore, + }); + + // Método para serializar a JSON + Map toJson() { + return { + 'user': user.toDocument(), + 'professionalInfo': professionalInfo.toDocument(), + 'averageScore': averageScore, + }; + } + + // Método para deserializar desde JSON + factory UsuarioProfesional.fromJson(Map json) { + return UsuarioProfesional( + user: Usuario.fromDocument(json['user']), + professionalInfo: Profesional.fromDocument(json['professionalInfo']), + averageScore: json['averageScore'].toDouble(), + ); + } + + @override + String toString() { + return '''UsuarioProfesional{ + user: $user, + professionalInfo: $professionalInfo, + averageScore: $averageScore + }'''; + } +} diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart new file mode 100644 index 0000000..9c22f8a --- /dev/null +++ b/lib/providers/auth_provider.dart @@ -0,0 +1,334 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:prosapp_web_app/models/comment_entity.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; + +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:provider/provider.dart'; + +enum AuthStatus { checking, authenticated, notAuthenticated } + +class AuthProvider extends ChangeNotifier { + Usuario? user; + Profesional? professional; + double userAverageScore = 0.0; + + AuthStatus authStatus = AuthStatus.checking; + final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + String? _verificationId; + + AuthProvider() { + isAuthenticated(); + } + + Future login(String email, String password) async { + try { + await _firebaseAuth.signInWithEmailAndPassword( + email: email, password: password); + + final userData = await _firestore + .collection('users') + .doc(_firebaseAuth.currentUser!.uid) + .get(); + user = Usuario.fromDocument(userData.data()!); + + authStatus = AuthStatus.authenticated; + + notifyListeners(); + + NavigationService.replaceTo(Flurorouter.dashboardRoute); + } catch (e) { + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + + NotificationsService.showSnackBarError( + 'Usuario o contraseña incorrectos'); + } + } + + Future verifyPhoneNumberForLink(String phoneNumber) async { + try { + await _firebaseAuth.verifyPhoneNumber( + phoneNumber: phoneNumber, + timeout: const Duration(seconds: 60), + verificationCompleted: (PhoneAuthCredential credential) async { + if (_firebaseAuth.currentUser != null) { + // Vincula el número de teléfono a la cuenta actual + await _firebaseAuth.currentUser!.linkWithCredential(credential); + NotificationsService.showSnackbar('Número vinculado exitosamente'); + authStatus = AuthStatus.authenticated; + notifyListeners(); + } + }, + verificationFailed: (FirebaseAuthException e) { + NotificationsService.showSnackBarError( + 'Error en la verificación del teléfono: ${e.message}'); + }, + codeSent: (String verificationId, int? resendToken) { + _verificationId = verificationId; + notifyListeners(); + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + } catch (e) { + NotificationsService.showSnackBarError('Error al enviar OTP'); + } + } + + Future linkPhoneWithOTP(String smsCode) async { + try { + final credential = PhoneAuthProvider.credential( + verificationId: _verificationId!, + smsCode: smsCode, + ); + + if (_firebaseAuth.currentUser != null) { + await _firebaseAuth.currentUser!.linkWithCredential(credential); + + await FirebaseFirestore.instance + .collection('users') + .doc(_firebaseAuth.currentUser!.uid) + .update({'phone': _firebaseAuth.currentUser!.phoneNumber}); + + NotificationsService.showSnackbar('Número vinculado exitosamente'); + + authStatus = AuthStatus.authenticated; + notifyListeners(); + NavigationService.replaceTo(Flurorouter.profileRoute); + } + } catch (e) { + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + NotificationsService.showSnackBarError('Error en la verificación OTP'); + } + } + + Future register(String email, String password, String name) async { + try { + UserCredential userCredential = + await _firebaseAuth.createUserWithEmailAndPassword( + email: email, + password: password, + ); + + User? user = userCredential.user; + if (user != null) { + await _firestore.collection('users').doc(user.uid).set({ + 'birthday': "", + 'city': "", + 'email': user.email, + 'gender': "", + 'id': user.uid, + 'name': name.trim(), + 'nickname': name.trim().toLowerCase(), + 'phone': "", + 'picture': "", + 'professional_state': 0, + 'token': "", + }); + + authStatus = AuthStatus.authenticated; + notifyListeners(); + + NavigationService.replaceTo(Flurorouter.dashboardRoute); + } + } catch (e) { + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + + NotificationsService.showSnackBarError('Email ya registrado'); + } + } + + Future isAuthenticated() async { + final User? firebaseUser = _firebaseAuth.currentUser; + + if (firebaseUser == null) { + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + return false; + } + final userData = + await _firestore.collection('users').doc(firebaseUser.uid).get(); + + if (userData.exists) { + user = Usuario.fromDocument(userData.data()!); + + userAverageScore = await _getUserAverageScore(user!.id); + + authStatus = AuthStatus.authenticated; + notifyListeners(); + return true; + } else { + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + return false; + } + } + + Future logout() async { + await _firebaseAuth.signOut(); + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + + Provider.of( + NavigationService.navigatorKey.currentContext!, + listen: false) + .logout(); + Provider.of( + NavigationService.navigatorKey.currentContext!, + listen: false) + .logout(); + + NavigationService.replaceTo(Flurorouter.phoneLoginRoute); + } + + void refreshUser() { + isAuthenticated(); + } + + Future verifyPhoneNumber(String phoneNumber) async { + try { + await _firebaseAuth.verifyPhoneNumber( + phoneNumber: phoneNumber, + timeout: const Duration(seconds: 60), + verificationCompleted: (PhoneAuthCredential credential) async { + await _firebaseAuth.signInWithCredential(credential); + authStatus = AuthStatus.authenticated; + notifyListeners(); + NavigationService.replaceTo(Flurorouter.dashboardRoute); + }, + verificationFailed: (FirebaseAuthException e) { + NotificationsService.showSnackBarError( + 'La verificación del teléfono falló: ${e.message}'); + }, + codeSent: (String verificationId, int? resendToken) { + _verificationId = verificationId; + notifyListeners(); + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + } catch (e) { + NotificationsService.showSnackBarError('Error al enviar OTP'); + } + } + + Future signInWithOTP(String smsCode) async { + try { + final credential = PhoneAuthProvider.credential( + verificationId: _verificationId!, + smsCode: smsCode, + ); + await _firebaseAuth.signInWithCredential(credential); + + User? currentUser = _firebaseAuth.currentUser; + + if (currentUser != null) { + final userData = + await _firestore.collection('users').doc(currentUser.uid).get(); + + if (userData.exists) { + user = Usuario.fromDocument(userData.data()!); + } else { + await _firestore.collection('users').doc(currentUser.uid).set({ + 'birthday': "", + 'city': "", + 'email': "", + 'gender': "", + 'id': currentUser.uid, + 'name': "", + 'nickname': "", + 'phone': currentUser.phoneNumber, + 'picture': "", + 'professional_state': 0, + 'token': "", + }); + + final newUserData = + await _firestore.collection('users').doc(currentUser.uid).get(); + user = Usuario.fromDocument(newUserData.data()!); + } + + authStatus = AuthStatus.authenticated; + notifyListeners(); + + NavigationService.replaceTo(Flurorouter.dashboardRoute); + } + } catch (e) { + authStatus = AuthStatus.notAuthenticated; + notifyListeners(); + NotificationsService.showSnackBarError('Error en la verificación OTP'); + } + } + + Future addEmailAndPassword(String email, String password) async { + try { + final User? currentUser = _firebaseAuth.currentUser; + + if (currentUser != null) { + await currentUser.updateEmail(email); + + await currentUser.updatePassword(password); + + await _firestore.collection('users').doc(currentUser.uid).update({ + 'email': email, + }); + + notifyListeners(); + + NotificationsService.showSnackbar( + 'Email y contraseña añadidos exitosamente'); + + NavigationService.replaceTo(Flurorouter.profileRoute); + } + } catch (e) { + if (e is FirebaseAuthException && e.code == 'email-already-in-use') { + NotificationsService.showSnackBarError('El correo ya existe'); + } + + if (e is FirebaseAuthException && e.code == 'invalid-email') { + NotificationsService.showSnackBarError('El correo no es vßlido'); + } + + if (e is FirebaseAuthException && e.code == 'weak-password') { + NotificationsService.showSnackBarError( + 'La contraseña debe tener al menos 6 caracteres'); + } + + if (e is FirebaseAuthException && e.code == 'requires-recent-login') { + NotificationsService.showSnackBarError( + 'Debes iniciar sesión recientemente antes de agregar email y contraseña'); + } + } + } + + Future _getUserAverageScore(String userId) async { + final querySnapshot = await FirebaseFirestore.instance + .collection('comments') + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: false) + .get(); + + if (querySnapshot.docs.isEmpty) { + return 0.0; + } + + final scores = querySnapshot.docs + .map((e) => CommentEntity.fromDocument(e.data()).score) + .toList(); + final averageScore = scores.reduce((a, b) => a + b) / scores.length; + return averageScore; + } +} diff --git a/lib/providers/calendar_services_provider.dart b/lib/providers/calendar_services_provider.dart new file mode 100644 index 0000000..492e47b --- /dev/null +++ b/lib/providers/calendar_services_provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/servicio_profesional.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; + +class CalendarServicesProvider extends ChangeNotifier { + List services = []; + ServicioProfesional? service; + bool isLoading = true; + + final _servicesCollection = FirebaseFirestore.instance.collection('services'); + final _usersCollection = FirebaseFirestore.instance.collection('users'); + + void logout() { + services = []; + service = null; + isLoading = false; + notifyListeners(); + } + + getServicesForProfessional(String userId) async { + try { + isLoading = true; + + final queryServices = await _servicesCollection + .where('professional_id', isEqualTo: userId) + .where('status', whereIn: [1, 3]).get(); + + final servicios = queryServices.docs.map(Service.fromDocument).toList(); + + final userIds = servicios.map((service) => service.userId).toList(); + + final queryUsers = await _usersCollection + .where(FieldPath.documentId, whereIn: userIds) + .get(); + + final users = + queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList(); + + final usersMap = {for (var doc in users) doc.id: doc}; + + final servicesMap = (servicios.map((service) { + final user = usersMap[service.userId]; + + return ServicioProfesional( + user: user!, + service: service, + ); + }).toList()); + + services = servicesMap + ..sort((a, b) { + final dateA = DateTime.parse(a.service.day); + final dateB = DateTime.parse(b.service.day); + final range1A = + a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute; + final range1B = + b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute; + + if (dateA.compareTo(dateB) != 0) { + return dateA.compareTo(dateB); + } else { + return range1A.compareTo(range1B); + } + }); + } catch (e) { + print('Error obteniendo servicios: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/providers/chat_provider.dart b/lib/providers/chat_provider.dart new file mode 100644 index 0000000..ed67cf3 --- /dev/null +++ b/lib/providers/chat_provider.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/chat_entity.dart'; +import 'package:prosapp_web_app/models/message_entity.dart'; +import 'package:prosapp_web_app/repositories/firebase_chat_repository.dart'; + +class ChatProvider with ChangeNotifier { + final FirebaseChatRepository _firebaseChatRepository = + FirebaseChatRepository(); + + ChatEntity? _currentChat; + ChatEntity? get currentChat => _currentChat; + + Stream getChat(String chatId) { + return _firebaseChatRepository.getChatById(chatId).map((chat) { + _currentChat = chat; + notifyListeners(); + return chat; + }); + } + + Future createChat( + String chatId, String userId, String professionalId) async { + final chat = await _firebaseChatRepository.createNewChat( + chatId, userId, professionalId); + _currentChat = chat; + notifyListeners(); + return chat; + } + + Future sendMessage(String chatId, MessageEntity message) async { + await _firebaseChatRepository.sendMessage(chatId, message); + notifyListeners(); + } +} diff --git a/lib/providers/cities_provider.dart b/lib/providers/cities_provider.dart new file mode 100644 index 0000000..2a00477 --- /dev/null +++ b/lib/providers/cities_provider.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosapp_web_app/models/city.dart'; +import 'package:prosapp_web_app/models/country_entity.dart'; + +class CitiesProvider extends ChangeNotifier { + List cities = []; + bool isLoading = true; + + final _citiesCollection = + FirebaseFirestore.instance.collection('countries v2'); + + CitiesProvider() { + getCities(); + } + + getCoordsOfCity(String cityName) { + if (cities.isEmpty) return null; + + for (var city in cities) { + if (city.cityName == cityName) { + return city.coordsOfCity; + } + } + return null; + } + + getCities() async { + try { + final querySnapshot = await _citiesCollection.doc('Colombia').get(); + + final data = querySnapshot.data(); + + final country = CountryEntity.fromDocument(data as Map); + + for (var region in country.regions) { + for (var city in region.cities) { + cities.add(City( + cityName: city.name, + coordsOfCity: city.coords, + stateOfCity: region.name, + countryOfCity: country.name, + )); + } + } + + cities.sort((a, b) => a.cityName.compareTo(b.cityName)); + } catch (e) { + print('Error obteniendo ciudades: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/providers/email_form_provider.dart b/lib/providers/email_form_provider.dart new file mode 100644 index 0000000..3fd6f7d --- /dev/null +++ b/lib/providers/email_form_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class EmailFormProvider extends ChangeNotifier { + GlobalKey formKey = GlobalKey(); + + String email = ''; + String password = ''; + + bool validateForm() { + if (formKey.currentState!.validate()) { + return true; + } else { + print('Formulario no válido'); + + return false; + } + } +} diff --git a/lib/providers/login_form_provider.dart b/lib/providers/login_form_provider.dart new file mode 100644 index 0000000..f6366fb --- /dev/null +++ b/lib/providers/login_form_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class LoginFormProvider extends ChangeNotifier { + GlobalKey formKey = GlobalKey(); + + String email = ""; + String password = ""; + + bool validateForm() { + if (formKey.currentState!.validate()) { + // print('Formulario válido'); + // print('Email: $email, password: $password'); + return true; + } else { + print('Formulario no válido'); + + return false; + } + } +} diff --git a/lib/providers/phone_form_provider.dart b/lib/providers/phone_form_provider.dart new file mode 100644 index 0000000..82f79f1 --- /dev/null +++ b/lib/providers/phone_form_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class PhoneFormProvider extends ChangeNotifier { + GlobalKey formKey = GlobalKey(); + + String phone = ""; + String code = ""; + + bool validateForm() { + if (formKey.currentState!.validate()) { + return true; + } else { + print('Formulario no válido'); + + return false; + } + } +} diff --git a/lib/providers/professional_detail_provider.dart b/lib/providers/professional_detail_provider.dart new file mode 100644 index 0000000..9d74006 --- /dev/null +++ b/lib/providers/professional_detail_provider.dart @@ -0,0 +1,72 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/comment_entity.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/models/usuario_profesional.dart'; + +class ProfessionalDetailProvider extends ChangeNotifier { + UsuarioProfesional? professional; + bool isLoading = true; + + Future getProfessionalById(String uid) async { + try { + isLoading = true; + notifyListeners(); + + final userDoc = + await FirebaseFirestore.instance.collection('users').doc(uid).get(); + + if (!userDoc.exists) { + print('Profesional no encontrado.'); + return; + } + + final user = Usuario.fromDocument(userDoc.data()!); + + final professionalDoc = await FirebaseFirestore.instance + .collection('professional_info') + .doc(uid) + .get(); + + if (!professionalDoc.exists) { + print('Información profesional no encontrada.'); + return; + } + + final professionalInfo = + Profesional.fromDocument(professionalDoc.data()!); + + final averageScore = await _getAverageScore(uid); + + professional = UsuarioProfesional( + user: user, + professionalInfo: professionalInfo, + averageScore: averageScore, + ); + } catch (e) { + print('Error obteniendo profesional: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future _getAverageScore(String userId) async { + final querySnapshot = await FirebaseFirestore.instance + .collection('comments') + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: true) + .get(); + + if (querySnapshot.docs.isEmpty) { + return 0.0; + } + + final scores = querySnapshot.docs + .map((e) => CommentEntity.fromDocument(e.data()).score) + .toList(); + final averageScore = scores.reduce((a, b) => a + b) / scores.length; + return averageScore; + } +} diff --git a/lib/providers/professional_form_provider.dart b/lib/providers/professional_form_provider.dart new file mode 100644 index 0000000..d292652 --- /dev/null +++ b/lib/providers/professional_form_provider.dart @@ -0,0 +1,200 @@ +import 'dart:typed_data'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/location_preferences.dart'; +import 'package:prosapp_web_app/models/payment_method_entity.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/schedules.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; + +class ProfessionalFormProvider with ChangeNotifier { + Profesional? profesional; + GlobalKey formKey = GlobalKey(); + GlobalKey profileFormKey = GlobalKey(); + + copyProfesionalWith({ + String? id, + String? identification, + String? address, + String? aditionalAddress, + String? profession, + bool ratePreferences = false, + String? rate, + LocationPreferences? locationPreferences, + String? bannerPicture, + String? identificationPicture, + String? certificatePicture, + double? latitude, + double? longitude, + List? specializations, + List? specializationsPictures, + Schedules? schedules, + PaymentMethodEntity? paymentMethods, + }) { + profesional = Profesional( + id: id ?? profesional!.id, + identification: identification ?? profesional!.identification, + address: address ?? profesional!.address, + aditionalAddress: aditionalAddress ?? profesional!.aditionalAddress, + profession: profession ?? profesional!.profession, + ratePreferences: ratePreferences, + rate: rate ?? profesional!.rate, + locationPreferences: + locationPreferences ?? profesional!.locationPreferences, + bannerPicture: bannerPicture ?? profesional!.bannerPicture, + identificationPicture: + identificationPicture ?? profesional!.identificationPicture, + certificatePicture: certificatePicture ?? profesional!.certificatePicture, + latitude: latitude ?? profesional!.latitude, + longitude: longitude ?? profesional!.longitude, + specializations: specializations ?? profesional!.specializations, + specializationsPictures: + specializationsPictures ?? profesional!.specializationsPictures, + schedules: schedules ?? profesional!.schedules, + paymentMethods: paymentMethods ?? profesional!.paymentMethods, + ); + + notifyListeners(); + } + + bool _validForm() { + return formKey.currentState!.validate(); + } + + bool _validProfileForm() { + return profileFormKey.currentState!.validate(); + } + + setProfesional(Profesional profesional) { + this.profesional = profesional; + notifyListeners(); + } + + Future updateProfesionalInfo(String userId) async { + if (!_validForm()) return false; + + final docProfessional = FirebaseFirestore.instance + .collection('professional_info') + .doc(userId) + .withConverter( + fromFirestore: (snapshot, _) => + Profesional.fromDocument(snapshot.data()!), + toFirestore: (user, _) => user.toDocument(), + ); + + await docProfessional.set(profesional!); + + NotificationsService.showSnackbar('Información actualizada'); + + return true; + } + + Future updateProfesionalProfileInfo(String userId) async { + if (!_validProfileForm()) return false; + + final docProfessional = FirebaseFirestore.instance + .collection('professional_info') + .doc(userId) + .withConverter( + fromFirestore: (snapshot, _) => + Profesional.fromDocument(snapshot.data()!), + toFirestore: (user, _) => user.toDocument(), + ); + + await docProfessional.set(profesional!); + + NotificationsService.showSnackbar('Información actualizada'); + + return true; + } + + Future updateProfesionalProfileScheduleInfo(String userId) async { + final docProfessional = FirebaseFirestore.instance + .collection('professional_info') + .doc(userId) + .withConverter( + fromFirestore: (snapshot, _) => + Profesional.fromDocument(snapshot.data()!), + toFirestore: (user, _) => user.toDocument(), + ); + + await docProfessional.set(profesional!); + + NotificationsService.showSnackbar('Información actualizada'); + + return true; + } + + Future uploadPdfIdentification( + Uint8List fileBytes, String userId) async { + try { + final storageRef = FirebaseStorage.instance + .ref() + .child('$userId/PDF/${userId}_cedula.pdf'); + + await storageRef.putData(fileBytes); + final url = await storageRef.getDownloadURL(); + + copyProfesionalWith(identificationPicture: url); + + notifyListeners(); + + return profesional!; + } catch (e) { + print("Error al subir el PDF de identificación: $e"); + NotificationsService.showSnackbar('Error al subir el PDF'); + rethrow; + } + } + + Future uploadPdfCertificate( + Uint8List fileBytes, String userId) async { + try { + final storageRef = FirebaseStorage.instance + .ref() + .child('$userId/PDF/${userId}_certificado.pdf'); + + await storageRef.putData(fileBytes); + final url = await storageRef.getDownloadURL(); + + copyProfesionalWith(certificatePicture: url); + + notifyListeners(); + + return profesional!; + } catch (e) { + print("Error al subir el PDF de identificación: $e"); + NotificationsService.showSnackbar('Error al subir el PDF'); + rethrow; + } + } + + Future uploadPdfSpecializations( + List filesBytes, String userId) async { + try { + List urls = []; + + for (int i = 0; i < filesBytes.length; i++) { + final storageRef = FirebaseStorage.instance.ref().child( + '$userId/PDF/${userId}_${DateTime.now().millisecondsSinceEpoch}_especializacion_$i.pdf'); + + await storageRef.putData(filesBytes[i]); + + final url = await storageRef.getDownloadURL(); + urls.add(url); + } + + copyProfesionalWith(specializationsPictures: urls); + + notifyListeners(); + + return profesional!; + } catch (e) { + print("Error al subir los PDFs de especialización: $e"); + NotificationsService.showSnackbar('Error al subir los PDFs'); + rethrow; + } + } +} diff --git a/lib/providers/professional_provider.dart b/lib/providers/professional_provider.dart new file mode 100644 index 0000000..7ff0fbd --- /dev/null +++ b/lib/providers/professional_provider.dart @@ -0,0 +1,69 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:prosapp_web_app/models/location_preferences.dart'; +import 'package:prosapp_web_app/models/payment_method_entity.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/schedules.dart'; + +class ProfessionalProvider extends ChangeNotifier { + Profesional? profesional; + bool _isProModeActive = false; + + final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; + + bool get isProModeActive { + return _isProModeActive; + } + + Future getProfessional(String uid) async { + try { + final professional = await FirebaseFirestore.instance + .collection('professional_info') + .doc(uid) + .get(); + + profesional = Profesional.fromDocument(professional.data()!); + } catch (e) { + profesional = Profesional( + id: _firebaseAuth.currentUser!.uid, + identification: '', + address: '', + aditionalAddress: '', + profession: '', + ratePreferences: false, + rate: '', + locationPreferences: LocationPreferences.office, + bannerPicture: '', + identificationPicture: '', + certificatePicture: '', + latitude: 0, + longitude: 0, + specializations: [], + specializationsPictures: [], + schedules: Schedules.empty, + paymentMethods: PaymentMethodEntity.empty, + ); + } + + notifyListeners(); + return profesional!; + } + + logout() { + _isProModeActive = false; + notifyListeners(); + } + + void toggleProMode() { + _isProModeActive = !_isProModeActive; + notifyListeners(); + } + + void setIsProModeActive(bool value) { + _isProModeActive = value; + notifyListeners(); + } + + sendProfessionalToReview() {} +} diff --git a/lib/providers/professionals_provider.dart b/lib/providers/professionals_provider.dart new file mode 100644 index 0000000..fc01955 --- /dev/null +++ b/lib/providers/professionals_provider.dart @@ -0,0 +1,80 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/comment_entity.dart'; +import 'package:prosapp_web_app/models/pro_state.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/models/usuario_profesional.dart'; + +class ProfessionalsProvider extends ChangeNotifier { + List professionals = []; + bool isLoading = true; + + ProfessionalsProvider() { + getProfessionals(); + } + + getProfessionals() async { + try { + final querySnapshot = await FirebaseFirestore.instance + .collection('users') + .where('professional_state', isEqualTo: ProState.active.index) + .get(); + + final users = querySnapshot.docs + .map((e) => Usuario.fromDocument(e.data())) + .toList(); + + final ids = users.map((e) => e.id).toList(); + + final querySnapshot2 = await FirebaseFirestore.instance + .collection('professional_info') + .where(FieldPath.documentId, whereIn: ids) + .get(); + + final professionalsInfo = querySnapshot2.docs + .map((e) => Profesional.fromDocument(e.data())) + .toList(); + + final professionalInfoMap = { + for (var doc in professionalsInfo) doc.id: doc + }; + + final usersMap = await Future.wait(users.map((user) async { + final professionalInfo = professionalInfoMap[user.id]; + final averageScore = await _getAverageScore(user.id); + return UsuarioProfesional( + user: user, + professionalInfo: professionalInfo!, + averageScore: averageScore, + ); + }).toList()); + + professionals = usersMap; + + } catch (e) { + print('Error obteniendo profesionales: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future _getAverageScore(String userId) async { + final querySnapshot = await FirebaseFirestore.instance + .collection('comments') + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: true) + .get(); + + if (querySnapshot.docs.isEmpty) { + return 0.0; + } + + final scores = querySnapshot.docs + .map((e) => CommentEntity.fromDocument(e.data()).score) + .toList(); + final averageScore = scores.reduce((a, b) => a + b) / scores.length; + return averageScore; + } +} diff --git a/lib/providers/professions_provider.dart b/lib/providers/professions_provider.dart new file mode 100644 index 0000000..f592af7 --- /dev/null +++ b/lib/providers/professions_provider.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosapp_web_app/models/profession.dart'; + +class ProfessionsProvider extends ChangeNotifier { + List professions = []; + bool isLoading = true; + + final _professionsCollection = + FirebaseFirestore.instance.collection('professions'); + + ProfessionsProvider() { + getProfessions(); + } + + getProfessions() async { + try { + final documentSnapshot = + await _professionsCollection.doc('professions').get(); + + final data = documentSnapshot.data() as Map; + + professions = (data['professions'] as List) + .map((item) => Profession(name: item as String)) + .toList(); + } catch (e) { + print('Error obteniendo profesiones: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/providers/profile_form_provider.dart b/lib/providers/profile_form_provider.dart new file mode 100644 index 0000000..f26f937 --- /dev/null +++ b/lib/providers/profile_form_provider.dart @@ -0,0 +1,159 @@ +import 'dart:typed_data'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/pro_state.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; + +class ProfileFormProvider extends ChangeNotifier { + Usuario? user; + GlobalKey formKey = GlobalKey(); + + String? _verificationId; + + void copyUserWith({ + String? id, + String? email, + String? phone, + String? name, + String? nickname, + String? city, + String? picture, + String? birthday, + String? gender, + ProState? proState, + String? token, + }) { + user = Usuario( + id: id ?? user!.id, + email: email ?? user!.email, + phone: phone ?? user!.phone, + name: name ?? user!.name, + nickname: nickname ?? user!.nickname, + city: city ?? user!.city, + picture: picture ?? user!.picture, + birthday: birthday ?? user!.birthday, + gender: gender ?? user!.gender, + proState: proState ?? user!.proState, + token: token ?? user!.token, + ); + notifyListeners(); + } + + bool _validForm() { + return formKey.currentState!.validate(); + } + + Future updateUserInfo() async { + if (!_validForm()) return; + + final docUser = FirebaseFirestore.instance + .collection('users') + .doc(user!.id) + .withConverter( + fromFirestore: (snapshot, _) => + Usuario.fromDocument(snapshot.data()!), + toFirestore: (user, _) => user.toDocument(), + ); + + await docUser.set(user!); + NotificationsService.showSnackbar('Información actualizada'); + } + + Future updateUserInfoNoValid() async { + final docUser = FirebaseFirestore.instance + .collection('users') + .doc(user!.id) + .withConverter( + fromFirestore: (snapshot, _) => + Usuario.fromDocument(snapshot.data()!), + toFirestore: (user, _) => user.toDocument(), + ); + + await docUser.set(user!); + NotificationsService.showSnackbar('Información actualizada'); + } + + Future uploadPicture(Uint8List bytes) async { + try { + final storageRef = FirebaseStorage.instance + .ref() + .child('${user!.id}/PP/${user!.id}_lead'); + + await storageRef.putData(bytes); + + final url = await storageRef.getDownloadURL(); + + copyUserWith(picture: url); + notifyListeners(); + + return user!; + } catch (e) { + print("Error al subir la imagen: $e"); + rethrow; + } + } + + // Agregar numero + Future signUpWithPhoneNumber(String phoneNumber) async { + try { + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: phoneNumber, + verificationCompleted: (PhoneAuthCredential credential) async { + await FirebaseAuth.instance.signInWithCredential(credential); + NotificationsService.showSnackbar('Autenticación exitosa'); + }, + verificationFailed: (FirebaseAuthException e) { + NotificationsService.showSnackbar( + 'Error en la verificación: ${e.message}'); + }, + codeSent: (String verificationId, int? resendToken) { + _verificationId = verificationId; + NotificationsService.showSnackbar( + 'Código enviado al número $phoneNumber'); + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + + NotificationsService.showSnackbar('Código enviado'); + } catch (e) { + NotificationsService.showSnackbar('Error al registrar con teléfono: $e'); + } + } + + Future linkPhoneNumberToExistingAccount( + String phoneNumber, String code) async { + try { + var phoneAuthCredential = PhoneAuthProvider.credential( + verificationId: _verificationId!, + smsCode: code, + ); + + User? user = FirebaseAuth.instance.currentUser; + + if (user != null) { + await user.linkWithCredential(phoneAuthCredential); + + copyUserWith(phone: phoneNumber); + notifyListeners(); + + NotificationsService.showSnackbar('Número vinculado exitosamente'); + return true; + } + return false; + } catch (e) { + if (e is FirebaseAuthException && e.code == 'invalid-verification-code') { + NotificationsService.showSnackbar('Código de verificación inválido'); + return false; + } else { + NotificationsService.showSnackbar('Error al vincular número: $e'); + rethrow; + } + } + } +} diff --git a/lib/providers/register_form_provider.dart b/lib/providers/register_form_provider.dart new file mode 100644 index 0000000..c131081 --- /dev/null +++ b/lib/providers/register_form_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class RegisterFormProvider extends ChangeNotifier { + GlobalKey formKey = GlobalKey(); + + String name = ""; + String email = ""; + String password = ""; + + validateForm() { + if (formKey.currentState!.validate()) { + print('Formulario válido'); + print('Name: $name, Email: $email, password: $password'); + return true; + } else { + print('Formulario no válido'); + + return false; + } + } +} diff --git a/lib/providers/score_provider.dart b/lib/providers/score_provider.dart new file mode 100644 index 0000000..5afc57c --- /dev/null +++ b/lib/providers/score_provider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/comment_entity.dart'; +import 'package:prosapp_web_app/models/score_entity.dart'; +import 'package:prosapp_web_app/repositories/firebase_score_repository.dart'; + +class ScoreProvider with ChangeNotifier { + final FirebaseScoreRepository _firebaseScoreRepository = + FirebaseScoreRepository(); + + ReputationEntity _reputation = const ReputationEntity( + total: 0, + average: 0, + totalPro: 0, + averagePro: 0, + ); + ReputationEntity get reputation => _reputation; + + List _comments = []; + List get comments => _comments; + + // Método para cargar reputación dinámica según el ID + Future loadReputation(String userId) async { + try { + final reputation = + await _firebaseScoreRepository.getReputationByUserId(userId); + _reputation = reputation; + notifyListeners(); + } catch (e) { + debugPrint('Error loading reputation: $e'); + } + } + + // Método para cargar comentarios dinámicamente + void loadComments(String userId, {required bool isProfessional}) { + final commentStream = isProfessional + ? _firebaseScoreRepository.getScoresForProfessional(userId) + : _firebaseScoreRepository.getScoresForUser(userId); + + commentStream.listen((comments) { + _comments = comments; + notifyListeners(); + }); + } + + Future addComment(CommentEntity comment) async { + try { + await _firebaseScoreRepository.addComment(comment); + notifyListeners(); + } catch (e) { + debugPrint('Error adding comment: $e'); + } + } +} diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart new file mode 100644 index 0000000..8ce6097 --- /dev/null +++ b/lib/providers/services_provider.dart @@ -0,0 +1,390 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosapp_web_app/models/pro_state.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; +import 'package:prosapp_web_app/models/servicio_profesional.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; + +class ServicesProvider extends ChangeNotifier { + List services = []; + ServicioProfesional? service; + bool isLoading = true; + + final _servicesCollection = FirebaseFirestore.instance.collection('services'); + final _usersCollection = FirebaseFirestore.instance.collection('users'); + + void logout() { + services = []; + service = null; + isLoading = false; + notifyListeners(); + } + + Future changeServiceStatus( + String serviceId, ServiceStatus newStatus) async { + try { + final statusValue = enumToIntService(newStatus); + await FirebaseFirestore.instance + .collection('services') + .doc(serviceId) + .update({'status': statusValue}); + notifyListeners(); + } catch (e) { + print('Error al actualizar el estado: $e'); + } + } + + // cambiar el user_scored a true + Future changeUserScored(String serviceId) async { + try { + await FirebaseFirestore.instance + .collection('services') + .doc(serviceId) + .update({'user_scored': true}); + notifyListeners(); + } catch (e) { + print('Error al actualizar el estado: $e'); + } + } + + // cambiar el professional_scored a true + Future changeProfessionalScored(String serviceId) async { + try { + await FirebaseFirestore.instance + .collection('services') + .doc(serviceId) + .update({'professional_scored': true}); + notifyListeners(); + } catch (e) { + print('Error al actualizar el estado: $e'); + } + } + + void clearServices() { + services.clear(); + notifyListeners(); + } + + getServiceForUser(String serviceId) async { + try { + isLoading = true; + + final queryService = await _servicesCollection.doc(serviceId).get(); + + final servicio = Service.fromDocument(queryService); + + final queryUsers = + await _usersCollection.doc(servicio.professionalId).get(); + + final user = Usuario.fromDocument(queryUsers.data()!); + + service = ServicioProfesional( + user: user, + service: servicio, + ); + } catch (e) { + service = null; + } finally { + isLoading = false; + notifyListeners(); + } + } + + getServiceForProfessional(String serviceId) async { + try { + isLoading = true; + + final queryService = await _servicesCollection.doc(serviceId).get(); + + final servicio = Service.fromDocument(queryService); + + final queryUsers = await _usersCollection.doc(servicio.userId).get(); + + final user = Usuario.fromDocument(queryUsers.data()!); + + service = ServicioProfesional( + user: user, + service: servicio, + ); + } catch (e) { + service = null; + } finally { + isLoading = false; + notifyListeners(); + } + } + + getServicesForUser(String userId) async { + try { + isLoading = true; + + final queryServices = await _servicesCollection + .where('user_id', isEqualTo: userId) + .where('status', whereIn: [0, 1, 3]).get(); + + final servicios = queryServices.docs.map(Service.fromDocument).toList(); + + final professionalIds = + servicios.map((service) => service.professionalId).toList(); + + final queryUsers = await _usersCollection + .where('professional_state', isEqualTo: ProState.active.index) + .where(FieldPath.documentId, whereIn: professionalIds) + .get(); + + final professionalUsers = + queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList(); + + final professionalUsersMap = { + for (var user in professionalUsers) user.id: user + }; + + final servicesMap = (servicios.map((service) { + final user = professionalUsersMap[service.professionalId]; + + return ServicioProfesional( + user: user!, + service: service, + ); + }).toList()); + + services = servicesMap + ..sort((a, b) { + final dateA = DateTime.parse(a.service.day); + final dateB = DateTime.parse(b.service.day); + final range1A = + a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute; + final range1B = + b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute; + + if (dateA.compareTo(dateB) != 0) { + return dateA.compareTo(dateB); + } else { + return range1A.compareTo(range1B); + } + }); + } catch (e) { + print('Error obteniendo servicios: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + getServicesRequestsForProfessional(String userId) async { + try { + isLoading = true; + + final queryServices = await _servicesCollection + .where('professional_id', isEqualTo: userId) + .where('status', whereIn: [0]).get(); + + final servicios = queryServices.docs.map(Service.fromDocument).toList(); + + final userIds = servicios.map((service) => service.userId).toList(); + + final queryUsers = await _usersCollection + .where(FieldPath.documentId, whereIn: userIds) + .get(); + + final users = + queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList(); + + final usersMap = {for (var doc in users) doc.id: doc}; + + final servicesMap = (servicios.map((service) { + final user = usersMap[service.userId]; + + return ServicioProfesional( + user: user!, + service: service, + ); + }).toList()); + + services = servicesMap + ..sort((a, b) { + final dateA = DateTime.parse(a.service.day); + final dateB = DateTime.parse(b.service.day); + final range1A = + a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute; + final range1B = + b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute; + + if (dateA.compareTo(dateB) != 0) { + return dateA.compareTo(dateB); + } else { + return range1A.compareTo(range1B); + } + }); + } catch (e) { + print('Error obteniendo servicios: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + getServicesForProfessional(String userId) async { + try { + isLoading = true; + + final queryServices = await _servicesCollection + .where('professional_id', isEqualTo: userId) + .where('status', whereIn: [1, 3]).get(); + + final servicios = queryServices.docs.map(Service.fromDocument).toList(); + + final userIds = servicios.map((service) => service.userId).toList(); + + final queryUsers = await _usersCollection + .where(FieldPath.documentId, whereIn: userIds) + .get(); + + final users = + queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList(); + + final usersMap = {for (var doc in users) doc.id: doc}; + + final servicesMap = (servicios.map((service) { + final user = usersMap[service.userId]; + + return ServicioProfesional( + user: user!, + service: service, + ); + }).toList()); + + services = servicesMap + ..sort((a, b) { + final dateA = DateTime.parse(a.service.day); + final dateB = DateTime.parse(b.service.day); + final range1A = + a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute; + final range1B = + b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute; + + if (dateA.compareTo(dateB) != 0) { + return dateA.compareTo(dateB); + } else { + return range1A.compareTo(range1B); + } + }); + } catch (e) { + print('Error obteniendo servicios: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + getServicesHistoryForUser(String userId) async { + try { + isLoading = true; + + final queryServices = await _servicesCollection + .where('user_id', isEqualTo: userId) + .where('status', whereIn: [2, 4, 5]).get(); + + final servicios = queryServices.docs.map(Service.fromDocument).toList(); + + final professionalIds = + servicios.map((service) => service.professionalId).toList(); + + final queryUsers = await _usersCollection + .where('professional_state', isEqualTo: ProState.active.index) + .where(FieldPath.documentId, whereIn: professionalIds) + .get(); + + final professionalUsers = + queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList(); + + final professionalUsersMap = { + for (var user in professionalUsers) user.id: user + }; + + final servicesMap = (servicios.map((service) { + final user = professionalUsersMap[service.professionalId]; + + return ServicioProfesional( + user: user!, + service: service, + ); + }).toList()); + + services = servicesMap + ..sort((a, b) { + final dateA = DateTime.parse(a.service.day); + final dateB = DateTime.parse(b.service.day); + final range1A = + a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute; + final range1B = + b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute; + + if (dateA.compareTo(dateB) != 0) { + return dateA.compareTo(dateB); + } else { + return range1A.compareTo(range1B); + } + }); + } catch (e) { + print('Error obteniendo servicios: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + getServicesHistoryForProfessional(String userId) async { + try { + isLoading = true; + + final queryServices = await _servicesCollection + .where('professional_id', isEqualTo: userId) + .where('status', whereIn: [2, 4, 5]).get(); + + final servicios = queryServices.docs.map(Service.fromDocument).toList(); + + final userIds = servicios.map((service) => service.userId).toList(); + + final queryUsers = await _usersCollection + .where(FieldPath.documentId, whereIn: userIds) + .get(); + + final users = + queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList(); + + final usersMap = {for (var doc in users) doc.id: doc}; + + final servicesMap = (servicios.map((service) { + final user = usersMap[service.userId]; + + return ServicioProfesional( + user: user!, + service: service, + ); + }).toList()); + + services = servicesMap + ..sort((a, b) { + final dateA = DateTime.parse(a.service.day); + final dateB = DateTime.parse(b.service.day); + final range1A = + a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute; + final range1B = + b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute; + + if (dateA.compareTo(dateB) != 0) { + return dateA.compareTo(dateB); + } else { + return range1A.compareTo(range1B); + } + }); + } catch (e) { + print('Error obteniendo servicios: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart new file mode 100644 index 0000000..dca64a9 --- /dev/null +++ b/lib/providers/settings_provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosapp_web_app/models/setting.dart'; + +class SettingsProvider extends ChangeNotifier { + Setting? settings; + bool isLoading = true; + + final _settingsCollection = FirebaseFirestore.instance.collection('settings'); + + SettingsProvider() { + getSettings(); + } + + getSettings() async { + try { + final querySnapshot = await _settingsCollection.doc('global').get(); + settings = Setting.fromDocument(querySnapshot.data()!); + } catch (e) { + print('Error obteniendo los settings: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/providers/sidemenu_provider.dart b/lib/providers/sidemenu_provider.dart new file mode 100644 index 0000000..956fc60 --- /dev/null +++ b/lib/providers/sidemenu_provider.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class SideMenuProvider extends ChangeNotifier { + static late AnimationController menuController; + static bool isOpen = false; + + String _currentPage = ''; + + String get currentPage { + return _currentPage; + } + + void setCurrentPageUrl(String routeName) { + _currentPage = routeName; + Future.delayed(const Duration(milliseconds: 100), () { + notifyListeners(); + }); + } + + static Animation movement = + Tween(begin: -200, end: 0).animate( + CurvedAnimation(parent: menuController, curve: Curves.easeInOut), + ); + + static Animation opacity = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: menuController, curve: Curves.easeInOut), + ); + + static void openMenu() { + isOpen = true; + menuController.forward(); + } + + static void closeMenu() { + isOpen = false; + menuController.reverse(); + } + + static void toggleMenu() { + (isOpen) ? menuController.reverse() : menuController.forward(); + + isOpen = !isOpen; + } +} diff --git a/lib/repositories/firebase_chat_repository.dart b/lib/repositories/firebase_chat_repository.dart new file mode 100644 index 0000000..662d069 --- /dev/null +++ b/lib/repositories/firebase_chat_repository.dart @@ -0,0 +1,43 @@ +import 'dart:developer'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosapp_web_app/models/chat_entity.dart'; +import 'package:prosapp_web_app/models/message_entity.dart'; + +class FirebaseChatRepository { + final chatCollection = FirebaseFirestore.instance.collection('chats'); + + Stream getChatById(String chatId) { + return chatCollection.doc(chatId).snapshots().map((snapshot) { + try { + if (snapshot.exists) { + return ChatEntity.fromDocument(snapshot.data()!); + } else { + return null; + } + } catch (e) { + log(e.toString()); + return null; + } + }); + } + + Future createNewChat( + String chatId, String userId, String professionalId) async { + ChatEntity chat = ChatEntity( + id: chatId, + userId: userId, + professionalId: professionalId, + messages: const [], + ); + + await chatCollection.doc(chatId).set(chat.toDocument()); + + return chat; + } + + sendMessage(String chatId, MessageEntity message) { + chatCollection.doc(chatId).update({ + 'messages': FieldValue.arrayUnion([message.toDocument()]) + }); + } +} diff --git a/lib/repositories/firebase_score_repository.dart b/lib/repositories/firebase_score_repository.dart new file mode 100644 index 0000000..0c8d2cc --- /dev/null +++ b/lib/repositories/firebase_score_repository.dart @@ -0,0 +1,91 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:prosapp_web_app/models/comment_entity.dart'; +import 'package:prosapp_web_app/models/score_entity.dart'; + +class FirebaseScoreRepository { + final reputationsCollection = + FirebaseFirestore.instance.collection('reputations'); + + final commentsCollection = FirebaseFirestore.instance.collection('comments'); + + Future getReputationByUserId(String userId) async { + try { + final snap = await reputationsCollection.doc(userId).get(); + return ReputationEntity.fromDocument(snap.data()!); + } catch (e) { + return const ReputationEntity( + total: 0, + average: 0, + totalPro: 0, + averagePro: 0, + ); + } + } + + Stream streamReputation() { + return FirebaseAuth.instance.userChanges().asyncMap((user) async { + if (user != null) { + return await getReputationByUserId(user.uid); + } else { + return const ReputationEntity( + total: 0, + average: 0, + totalPro: 0, + averagePro: 0, + ); + } + }); + } + + Stream> getScoresForUser(String userId) { + return commentsCollection + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: false) + .snapshots() + .map((querySnapshot) => querySnapshot.docs + .map((doc) => CommentEntity.fromDocument(doc.data())) + .toList()); + } + + Stream> getScoresForProfessional(String userId) { + return commentsCollection + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: true) + .snapshots() + .map((querySnapshot) => querySnapshot.docs + .map((doc) => CommentEntity.fromDocument(doc.data())) + .toList()); + } + + Future addComment(CommentEntity comment) async { + await commentsCollection.add(comment.toDocument()); + + final query = await commentsCollection + .where('destination_id', isEqualTo: comment.destinationId) + .where('is_from_user', isEqualTo: comment.isFromUser) + .get(); + + var total = 0.0; + var count = 0; + for (var doc in query.docs) { + final comment = CommentEntity.fromDocument(doc.data()); + total += comment.score; + count++; + } + + if (count > 0) { + final average = total / count; + if (comment.isFromUser) { + await reputationsCollection.doc(comment.destinationId).set( + {'total_pro': count, 'average_pro': average}, + SetOptions(merge: true)); + } else { + await reputationsCollection.doc(comment.destinationId).set({ + 'total': count, + 'average': average, + }, SetOptions(merge: true)); + } + } + } +} diff --git a/lib/router/admin_handlers.dart b/lib/router/admin_handlers.dart new file mode 100644 index 0000000..c7f7bb4 --- /dev/null +++ b/lib/router/admin_handlers.dart @@ -0,0 +1,39 @@ +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/ui/views/dashboard_view.dart'; +import 'package:prosapp_web_app/ui/views/login_view.dart'; +import 'package:prosapp_web_app/ui/views/phone_login_view.dart'; +import 'package:prosapp_web_app/ui/views/register_view.dart'; +import 'package:fluro/fluro.dart'; +import 'package:provider/provider.dart'; + +class AdminHandlers { + static Handler phone = Handler(handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + + if (authProvider.authStatus == AuthStatus.notAuthenticated) { + return const PhoneLoginView(); + } else { + return const DashboardView(); + } + }); + + static Handler login = Handler(handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + + if (authProvider.authStatus == AuthStatus.notAuthenticated) { + return const LoginView(); + } else { + return const DashboardView(); + } + }); + + static Handler register = Handler(handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + + if (authProvider.authStatus == AuthStatus.notAuthenticated) { + return const RegisterView(); + } else { + return const DashboardView(); + } + }); +} diff --git a/lib/router/dashboard_handlers.dart b/lib/router/dashboard_handlers.dart new file mode 100644 index 0000000..d2cbf96 --- /dev/null +++ b/lib/router/dashboard_handlers.dart @@ -0,0 +1,404 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/ui/views/calendar_view.dart'; +import 'package:prosapp_web_app/ui/views/chat_view.dart'; +import 'package:prosapp_web_app/ui/views/email_view.dart'; +import 'package:prosapp_web_app/ui/views/phone_view.dart'; +import 'package:prosapp_web_app/ui/views/professional_calendar_view.dart'; +import 'package:prosapp_web_app/ui/views/dashboard_view.dart'; +import 'package:prosapp_web_app/ui/views/login_view.dart'; +import 'package:prosapp_web_app/ui/views/no_page_found_view.dart'; +import 'package:prosapp_web_app/ui/views/professional_profile_view.dart'; +import 'package:prosapp_web_app/ui/views/professionals_view.dart'; +import 'package:prosapp_web_app/ui/views/profile_view.dart'; +import 'package:prosapp_web_app/ui/views/rating_view.dart'; +import 'package:prosapp_web_app/ui/views/request_professional_view.dart'; +import 'package:prosapp_web_app/ui/views/schedule_view.dart'; +import 'package:prosapp_web_app/ui/views/service_view.dart'; +import 'package:prosapp_web_app/ui/views/services_history_view.dart'; +import 'package:prosapp_web_app/ui/views/services_requests_view.dart'; +import 'package:prosapp_web_app/ui/views/services_view.dart'; +import 'package:prosapp_web_app/ui/views/support_view.dart'; +import 'package:provider/provider.dart'; + +class DashboardHandlers { + static Handler dashboard = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.dashboardRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const DashboardView(); + } else { + return const LoginView(); + } + }, + ); + + static Handler support = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.supportRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const SupportView(); + } else { + return const LoginView(); + } + }, + ); + + // users + static Handler profile = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.profileRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ProfileView(); + } else { + return const LoginView(); + } + }, + ); + static Handler phone = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.phoneRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const PhoneView(); + } else { + return const LoginView(); + } + }, + ); + static Handler email = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.emailRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const EmailView(); + } else { + return const LoginView(); + } + }, + ); + static Handler professionals = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalsRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ProfessionalsView(); + } else { + return const LoginView(); + } + }, + ); + + static Handler userServices = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.userServicesRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ServicesView(type: 'user'); + } else { + return const LoginView(); + } + }, + ); + static Handler professionalServices = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalServicesRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ServicesView(type: 'professional'); + } else { + return const LoginView(); + } + }, + ); + + static Handler userService = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.userServiceRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['uid']?.first != null) { + return ServiceView(type: 'user', serviceId: params['uid']!.first); + } else { + // return const NoPageFoundView(); + return Center(child: Text('No se encontró el servicio.')); + } + } else { + return const LoginView(); + } + }, + ); + static Handler professionalService = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalServiceRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['uid']?.first != null) { + return ServiceView( + type: 'professional', serviceId: params['uid']!.first); + } else { + return Center(child: Text('No se encontró el servicio.')); + // return const NoPageFoundView(); + } + } else { + return const LoginView(); + } + }, + ); + static Handler userRating = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.userRatingRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['serviceId']?.first != null && + params['professionalId']?.first != null) { + final serviceId = params['serviceId']!.first; + final professionalId = params['professionalId']!.first; + + return RatingView( + type: 'user', + serviceId: serviceId, + professionalId: professionalId); + } else { + return const NoPageFoundView(); + } + } else { + return const LoginView(); + } + }, + ); + static Handler professionalRating = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalRatingRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['serviceId']?.first != null && + params['userId']?.first != null) { + final serviceId = params['serviceId']!.first; + final userId = params['userId']!.first; + + return RatingView( + type: 'professional', + serviceId: serviceId, + professionalId: userId); + } else { + return const NoPageFoundView(); + } + } else { + return const LoginView(); + } + }, + ); + static Handler userChat = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.userChatRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['serviceId']?.first != null && + params['professionalId']?.first != null) { + final serviceId = params['serviceId']!.first; + final professionalId = params['professionalId']!.first; + + return ChatView( + type: 'user', + serviceId: serviceId, + professionalId: professionalId); + } else { + return const NoPageFoundView(); + } + } else { + return const LoginView(); + } + }, + ); + static Handler professionalChat = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalChatRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['serviceId']?.first != null && + params['userId']?.first != null) { + final serviceId = params['serviceId']!.first; + final userId = params['userId']!.first; + + return ChatView( + type: 'professional', + serviceId: serviceId, + professionalId: userId); + } else { + return const NoPageFoundView(); + } + } else { + return const LoginView(); + } + }, + ); + + static Handler userServicesHistory = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.userServicesHistoryRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ServicesHistoryView(type: 'user'); + } else { + return const LoginView(); + } + }, + ); + static Handler professionalServicesRequests = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalServicesRequestsRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ServicesRequestsView(); + } else { + return const LoginView(); + } + }, + ); + static Handler professionalServicesHistory = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalServicesHistoryRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ServicesHistoryView(type: 'professional'); + } else { + return const LoginView(); + } + }, + ); + static Handler requestProfessional = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.requestProfessionalRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const RequestProfessionalView(); + } else { + return const LoginView(); + } + }, + ); + + // static Handler servicesHistory = Handler( + // handlerFunc: (context, params) { + // final authProvider = Provider.of(context!); + // Provider.of(context, listen: false) + // .setCurrentPageUrl(Flurorouter.servicesRoute); + + // if (authProvider.authStatus == AuthStatus.authenticated) { + // if (params['type']?.first != null && + // (params['type']?.first == 'user' || + // params['type']?.first == 'professional')) { + // return ServicesHistoryView(type: params['type']!.first); + // } else { + // return const NoPageFoundView(); + // } + // } else { + // return const LoginView(); + // } + // }, + // ); + + // professionals + static Handler professionalProfile = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalProfileRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ProfessionalProfileView(); + } else { + return const LoginView(); + } + }, + ); + static Handler professionalSchedule = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalProfileRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ScheduleView(); + } else { + return const LoginView(); + } + }, + ); + + static Handler calendar = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.calendarRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + if (params['uid']?.first != null) { + return CalendarView(professionalId: params['uid']!.first); + } else { + return const NoPageFoundView(); + } + } else { + return const LoginView(); + } + }, + ); + + static Handler professionalCalendar = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + Provider.of(context, listen: false) + .setCurrentPageUrl(Flurorouter.professionalCalendarRoute); + + if (authProvider.authStatus == AuthStatus.authenticated) { + return const ProfessionalCalendarView(); + } else { + return const LoginView(); + } + }, + ); +} diff --git a/lib/router/no_page_found_handlers.dart b/lib/router/no_page_found_handlers.dart new file mode 100644 index 0000000..e866bf3 --- /dev/null +++ b/lib/router/no_page_found_handlers.dart @@ -0,0 +1,15 @@ +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/ui/views/no_page_found_view.dart'; +import 'package:fluro/fluro.dart'; +import 'package:provider/provider.dart'; + +class NoPageFoundHandlers { + static Handler noPageFound = Handler( + handlerFunc: (context, params) { + Provider.of(context!, listen: false) + .setCurrentPageUrl('/404'); + + return const NoPageFoundView(); + }, + ); +} diff --git a/lib/router/router.dart b/lib/router/router.dart new file mode 100644 index 0000000..64f01ca --- /dev/null +++ b/lib/router/router.dart @@ -0,0 +1,196 @@ +import 'package:prosapp_web_app/router/admin_handlers.dart'; +import 'package:prosapp_web_app/router/dashboard_handlers.dart'; +import 'package:prosapp_web_app/router/no_page_found_handlers.dart'; +import 'package:fluro/fluro.dart'; + +class Flurorouter { + static final FluroRouter router = FluroRouter(); + + static String rootRoute = "/"; + + // Auth Router + static String phoneLoginRoute = "/auth/phone_login"; + static String loginRoute = "/auth/login"; + static String registerRoute = "/auth/register"; + + // Dashboard + static String dashboardRoute = "/dashboard"; + static String supportRoute = "/dashboard/support"; + + // Users + static String profileRoute = "/dashboard/profile"; + static String phoneRoute = "/dashboard/phone"; + static String emailRoute = "/dashboard/email"; + static String userServicesRoute = "/dashboard/user/services"; + static String userServicesHistoryRoute = "/dashboard/user/services/history"; + static String userServiceRoute = "/dashboard/user/service/:uid"; + static String calendarRoute = "/dashboard/calendar/:uid"; + static String requestProfessionalRoute = "/dashboard/request/professional"; + + static String userRatingRoute = "/dashboard/user/service/:serviceId/rating/:professionalId"; + static String professionalRatingRoute = "/dashboard/professional/service/:serviceId/rating/:userId"; + static String userChatRoute = "/dashboard/user/service/:serviceId/chat/:professionalId"; + static String professionalChatRoute = "/dashboard/professional/service/:serviceId/chat/:userId"; + + // Professionals + static String professionalProfileRoute = "/dashboard/professional/profile"; + static String professionalScheduleRoute = "/dashboard/professional/schedule"; + static String professionalServicesRoute = "/dashboard/professional/services"; + static String professionalServicesRequestsRoute = + "/dashboard/professional/requests"; + static String professionalServicesHistoryRoute = + "/dashboard/professional/services/history"; + static String professionalServiceRoute = + "/dashboard/professional/service/:uid"; + static String professionalsRoute = "/dashboard/professionals"; + static String professionalCalendarRoute = "/dashboard/professional/calendar"; + + static void configureRoutes() { + // Auth Routes + router.define( + rootRoute, + handler: AdminHandlers.phone, + transitionType: TransitionType.none, + ); + router.define( + phoneLoginRoute, + handler: AdminHandlers.phone, + transitionType: TransitionType.none, + ); + router.define( + loginRoute, + handler: AdminHandlers.login, + transitionType: TransitionType.none, + ); + router.define( + registerRoute, + handler: AdminHandlers.register, + transitionType: TransitionType.none, + ); + + // Dashboard Routes + router.define( + dashboardRoute, + handler: DashboardHandlers.dashboard, + transitionType: TransitionType.none, + ); + router.define( + supportRoute, + handler: DashboardHandlers.support, + transitionType: TransitionType.none, + ); + + // users + router.define( + profileRoute, + handler: DashboardHandlers.profile, + transitionType: TransitionType.none, + ); + router.define( + phoneRoute, + handler: DashboardHandlers.phone, + transitionType: TransitionType.none, + ); + router.define( + emailRoute, + handler: DashboardHandlers.email, + transitionType: TransitionType.none, + ); + router.define( + professionalsRoute, + handler: DashboardHandlers.professionals, + transitionType: TransitionType.none, + ); + router.define( + userServicesRoute, + handler: DashboardHandlers.userServices, + transitionType: TransitionType.none, + ); + router.define( + userServicesHistoryRoute, + handler: DashboardHandlers.userServicesHistory, + transitionType: TransitionType.none, + ); + + router.define( + userServiceRoute, + handler: DashboardHandlers.userService, + transitionType: TransitionType.none, + ); + router.define( + professionalServiceRoute, + handler: DashboardHandlers.professionalService, + transitionType: TransitionType.none, + ); + + router.define( + userRatingRoute, + handler: DashboardHandlers.userRating, + transitionType: TransitionType.none, + ); + router.define( + professionalRatingRoute, + handler: DashboardHandlers.professionalRating, + transitionType: TransitionType.none, + ); + + router.define( + userChatRoute, + handler: DashboardHandlers.userChat, + transitionType: TransitionType.none, + ); + router.define( + professionalChatRoute, + handler: DashboardHandlers.professionalChat, + transitionType: TransitionType.none, + ); + + router.define( + calendarRoute, + handler: DashboardHandlers.calendar, + transitionType: TransitionType.none, + ); + + router.define( + requestProfessionalRoute, + handler: DashboardHandlers.requestProfessional, + transitionType: TransitionType.none, + ); + + // profesionals + router.define( + professionalProfileRoute, + handler: DashboardHandlers.professionalProfile, + transitionType: TransitionType.none, + ); + router.define( + professionalScheduleRoute, + handler: DashboardHandlers.professionalSchedule, + transitionType: TransitionType.none, + ); + router.define( + professionalServicesRoute, + handler: DashboardHandlers.professionalServices, + transitionType: TransitionType.none, + ); + router.define( + professionalServicesRequestsRoute, + handler: DashboardHandlers.professionalServicesRequests, + transitionType: TransitionType.none, + ); + router.define( + professionalServicesHistoryRoute, + handler: DashboardHandlers.professionalServicesHistory, + transitionType: TransitionType.none, + ); + + router.define( + professionalCalendarRoute, + handler: DashboardHandlers.professionalCalendar, + transitionType: TransitionType.none, + ); + + // 404 + router.notFoundHandler = NoPageFoundHandlers.noPageFound; + } +} diff --git a/lib/services/local_storage.dart b/lib/services/local_storage.dart new file mode 100644 index 0000000..7f2b8fb --- /dev/null +++ b/lib/services/local_storage.dart @@ -0,0 +1,9 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class LocalStorage { + static late SharedPreferences prefs; + + static Future configurePrefs() async { + prefs = await SharedPreferences.getInstance(); + } +} diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart new file mode 100644 index 0000000..c8e0d13 --- /dev/null +++ b/lib/services/navigation_service.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class NavigationService { + static GlobalKey navigatorKey = GlobalKey(); + + static navigateTo(String routeName) { + navigatorKey.currentState!.pushNamed(routeName); + } + + static Future navigateToFuture(String routeName) async { + return await navigatorKey.currentState!.pushNamed(routeName); + } + + static replaceTo(String routeName) { + navigatorKey.currentState!.pushReplacementNamed(routeName); + } +} diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart new file mode 100644 index 0000000..8fbd82c --- /dev/null +++ b/lib/services/notifications_service.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class NotificationsService { + static GlobalKey messengerKey = GlobalKey(); + + static showSnackBarError(String message) { + final snackBar = SnackBar( + backgroundColor: Colors.red.withOpacity(0.9), + content: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + ), + ), + ); + + messengerKey.currentState! + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + + static showSnackbar(String message) { + final snackBar = SnackBar( + backgroundColor: Colors.blue.withOpacity(0.9), + content: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + ), + ), + ); + + messengerKey.currentState! + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + + static showBusyIndicator(BuildContext context) { + const AlertDialog dialog = AlertDialog( + content: SizedBox( + width: 100, + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + + showDialog(context: context, builder: (_) => dialog); + } +} diff --git a/lib/src/authentication/authentication_repository.dart b/lib/src/authentication/authentication_repository.dart deleted file mode 100644 index 2a991c3..0000000 --- a/lib/src/authentication/authentication_repository.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/presentation/screens/login/login.dart'; -import 'package:prosappco/src/presentation/screens/map/service.dart'; -import 'package:prosappco/src/presentation/screens/service_web.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; - -class AuthenticationRepository { - static final AuthenticationRepository instance = AuthenticationRepository._(); - AuthenticationRepository._() { - WidgetsBinding.instance.addPostFrameCallback((_) => _checkSession()); - } - - final _api = ApiService.instance; - - UserModel? currentUser; - bool isLoggedIn = false; - - Future _checkSession() async { - final token = await _api.getToken(); - if (token == null) { - pushOffAll(const LoginScreen()); - return; - } - try { - final data = await _api.get('/auth/me'); - currentUser = UserModel.fromApi(data as Map); - isLoggedIn = true; - _navigateHome(); - } catch (_) { - await _api.clearToken(); - pushOffAll(const LoginScreen()); - } - } - - void _navigateHome() { - kIsWeb - ? pushOffAll(const ServiceWebScreen()) - : pushOffAll(const ServiceScreen()); - } - - Future loginWithEmailAndPassword(String email, String password) async { - final data = await _api.post('/auth/login', {'email': email, 'password': password}); - await _api.saveToken(data['access_token']); - currentUser = UserModel.fromApi(data['user'] as Map); - isLoggedIn = true; - _navigateHome(); - } - - Future createUserWithEmailAndPassword(String email, String password, String name) async { - final data = await _api.post('/auth/register', { - 'email': email, - 'password': password, - 'name': name, - }); - await _api.saveToken(data['access_token']); - currentUser = UserModel.fromApi(data['user'] as Map); - isLoggedIn = true; - _navigateHome(); - } - - Future phoneAuthentication(String phone, {String? name}) async { - final data = await _api.post('/auth/phone', {'phone': phone, 'name': name ?? phone}); - await _api.saveToken(data['access_token']); - currentUser = UserModel.fromApi(data['user'] as Map); - isLoggedIn = true; - _navigateHome(); - } - - Future logout() async { - await _api.clearToken(); - currentUser = null; - isLoggedIn = false; - pushOffAll(const LoginScreen()); - } - - Future signInWithGoogle() async { - throw UnimplementedError('Google Sign-In no disponible en esta versión'); - } - - String? getCurrentUserUid() => currentUser?.id; - String? getCurrentUserPhone() => currentUser?.phoneNumber; -} diff --git a/lib/src/authentication/exceptions/register_failed.dart b/lib/src/authentication/exceptions/register_failed.dart deleted file mode 100644 index f2c6fd4..0000000 --- a/lib/src/authentication/exceptions/register_failed.dart +++ /dev/null @@ -1,19 +0,0 @@ -class SignUpWithEmailAndPasswordFailure { - final String message; - - const SignUpWithEmailAndPasswordFailure( - [this.message = "An Unknown error ocurred."]); - - factory SignUpWithEmailAndPasswordFailure.code(String code) { - switch (code) { - case 'weak-password': - return const SignUpWithEmailAndPasswordFailure( - 'Please enter a stronger password.'); - case 'email-alredy-in-use': - return const SignUpWithEmailAndPasswordFailure( - 'An account alredy exists for that email.'); - default: - return const SignUpWithEmailAndPasswordFailure(); - } - } -} diff --git a/lib/src/components/banner_photo.dart b/lib/src/components/banner_photo.dart deleted file mode 100644 index d58d1c3..0000000 --- a/lib/src/components/banner_photo.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/column_padding.dart'; - -const double photoSize = 150; - -class ReferenceBannerPhoto extends StatelessWidget { - /// Accepts either a String URL or null. - final Object? ref; - final double size; - final double sizeCircle; - - const ReferenceBannerPhoto({ - super.key, - required this.ref, - this.size = photoSize, - this.sizeCircle = photoSize, - }); - - @override - Widget build(BuildContext context) { - final url = ref is String ? ref as String : null; - - if (url == null || url.isEmpty || url.startsWith('...')) { - return DefaultPhoto(sizeDefault: sizeCircle); - } - - return Image.network( - url, - width: double.infinity, - height: size, - fit: BoxFit.fill, - errorBuilder: (_, __, ___) => DefaultPhoto(sizeDefault: sizeCircle), - ); - } -} - -class DefaultPhoto extends StatelessWidget { - final double sizeDefault; - - const DefaultPhoto({ - super.key, - this.sizeDefault = photoSize, - }); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 5, - offset: const Offset(0, 1), - ), - ], - ), - height: 150, - child: ColumnPadding( - alineacion: MainAxisAlignment.spaceAround, - padding: const EdgeInsets.symmetric(horizontal: 70), - children: [ - Container( - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(50), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 5, - offset: const Offset(0, 3), - ), - ], - ), - constraints: const BoxConstraints(minWidth: 250, minHeight: 50), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: Text( - 'Foto portada', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only(right: 15), - child: Icon( - Icons.file_upload_outlined, - color: Color(0xFF2BA4EC), - size: 30, - ), - ), - ], - ), - ), - ], - ), - ); - } -} - -class LocalPhoto extends StatelessWidget { - final File file; - - const LocalPhoto({super.key, required this.file}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Image.file( - file, - width: double.infinity, - height: photoSize, - fit: BoxFit.fill, - ), - ); - } -} diff --git a/lib/src/components/bottom_sheet.dart b/lib/src/components/bottom_sheet.dart deleted file mode 100644 index e6a1388..0000000 --- a/lib/src/components/bottom_sheet.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:animate_do/animate_do.dart'; - -class BottomSheetExpanded extends StatelessWidget { - final List children; - final double horizontalPadding; - - const BottomSheetExpanded({ - Key? key, - required this.children, - this.horizontalPadding = 35, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - colors: [ - Color.fromARGB(255, 139, 224, 255), - Color.fromARGB(255, 152, 228, 255), - Color(0xFFD6F4FF), - ], - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 50), - Center( - child: FadeInLeft( - duration: const Duration(milliseconds: 1000), - child: const Image( - image: AssetImage('images/logo_prosapp.png'), - ), - ), - ), - const SizedBox(height: 30), - Expanded( - child: FadeInUpBig( - duration: const Duration(milliseconds: 1000), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(60), - topRight: Radius.circular(60), - ), - ), - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - vertical: 15, - ), - child: SingleChildScrollView( - child: Column(children: children)), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/components/column_padding.dart b/lib/src/components/column_padding.dart deleted file mode 100644 index fffe675..0000000 --- a/lib/src/components/column_padding.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; - -class ColumnPadding extends StatelessWidget { - final List children; - final EdgeInsetsGeometry padding; - final MainAxisAlignment alineacion; - - const ColumnPadding({ - super.key, - required this.children, - required this.padding, - required this.alineacion, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: Column(mainAxisAlignment: alineacion, children: children)); - } -} diff --git a/lib/src/components/drawer_professional.dart b/lib/src/components/drawer_professional.dart deleted file mode 100644 index 79016f2..0000000 --- a/lib/src/components/drawer_professional.dart +++ /dev/null @@ -1,407 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/presentation/screens/calendar.dart'; -import 'package:prosappco/src/presentation/screens/my_services_pro.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile_pro.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile_pro_web.dart'; -import 'package:prosappco/src/presentation/screens/reputacion_pro.dart'; -import 'package:prosappco/src/presentation/screens/web_view.dart'; -import '../models/scores_model.dart'; -import '../presentation/screens/configuracion.dart'; -import '../presentation/screens/support.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class DrawerProfessional extends StatefulWidget { - @override - State createState() => _DrawerProfessionalState(); -} - -class _DrawerProfessionalState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - ScoresModel? scoresModel; - - Future _irSugerencias() async { - const url = 'https://admin.prosapp.co/sugerencias'; - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'No se pudo abrir la URL $url'; - } - } - - @override - void initState() { - super.initState(); - - if (scoresModel == null) { - ScoresModel.scoreTo(uid.toString(), true, false).then( - (ScoresModel s) => setState(() => scoresModel = s), - ); - } - } - - @override - Widget build(BuildContext context) { - // Use in-memory currentUser — no Firebase needed - final UserModel? currentUser = - AuthenticationRepository.instance.currentUser; - - return Drawer( - child: Container( - color: const Color(0xFFE9F9FF), - child: Column( - children: [ - Container( - color: Colors.white, - child: Column( - children: [ - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfileScreen(); - }, - ), - ); - }, - title: Text(currentUser?.name ?? '', - style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - currentUser?.phoneNumber ?? '', - style: const TextStyle(fontSize: 12), - ), - Text( - currentUser?.city ?? '', - style: const TextStyle(fontSize: 12), - ), - ], - ), - leading: ReferencePhoto( - ref: currentUser?.picture, - size: 55, - sizeCircle: 60, - ), - trailing: const Icon(Icons.keyboard_arrow_right, - color: Colors.black), - contentPadding: const EdgeInsets.symmetric( - vertical: 20, horizontal: 16), - ), - ], - ), - ), - Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 0), - ), - ], - ), - child: Divider( - height: 0, - color: Colors.grey[300], - ), - ), - Expanded( - child: Column( - children: [ - ListTile( - onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return MyServicesProScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.history, - color: Colors.black, - ), - title: const Text( - 'Mis servicios', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - if (kIsWeb) { - return const ProfileProWebScreen(); - } else { - return const ProfileProScreen(); - } - }, - ), - ); - }, - leading: const Icon( - Icons.person_outline, - color: Colors.black, - ), - title: const Text( - 'Perfil profesional', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ConfiguracionScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.construction_outlined, - color: Colors.black, - ), - title: const Text( - 'Configuración', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const SupportScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.question_mark_rounded, - color: Colors.black, - ), - title: const Text( - 'Soporte', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - if (kIsWeb) { - _irSugerencias(); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Sugerencias', - link: 'https://admin.prosapp.co/sugerencias'); - }, - ), - ); - } - }, - leading: const Icon( - Icons.campaign_outlined, - color: Colors.black, - ), - title: const Text( - 'Sugerencias', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const CalendarScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.calendar_month, - color: Colors.black, - ), - title: const Text( - 'Calendario', - style: TextStyle(fontSize: 15), - ), - ), - Builder(builder: (BuildContext context) { - return Container( - color: const Color(0xFF2BA4EC), - child: ListTile( - onTap: () { - if (ModalRoute.of(context)?.settings.name != - '/solicitud') { - Navigator.pushNamed(context, '/solicitud'); - } else { - Scaffold.of(context).openEndDrawer(); - } - }, - trailing: const Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - ), - title: const Text( - 'Solicitudes', - style: TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.bold), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 5, horizontal: 16), - ), - ); - }), - Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 3, - offset: const Offset(0, 2), - ), - ], - ), - child: Container( - color: Colors.white, - child: ListTile( - onTap: () async { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ReputationProScreen(); - }, - ), - ); - }, - trailing: const Icon(Icons.keyboard_arrow_right, - color: Colors.black), - title: const Text( - 'Reputación', - style: TextStyle(color: Colors.black), - ), - subtitle: Row( - children: [ - RatingBar.builder( - initialRating: scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: - const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${scoresModel?.total.toString()}) ${scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Prosapp', - style: TextStyle(fontSize: 10, color: Colors.grey[700]), - ), - Padding( - padding: - const EdgeInsets.only(top: 7, left: 3, right: 3), - child: Text( - '®', - style: - TextStyle(fontSize: 25, color: Colors.grey[700]), - ), - ), - Text( - 'todos los derechos reservados', - style: TextStyle(fontSize: 10, color: Colors.grey[700]), - ), - ], - ), - ], - ), - ), - ElevatedButton( - onPressed: () { - Navigator.pushReplacementNamed(context, '/servicio'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 45), - ), - child: const Text( - 'Modo usuario', - style: TextStyle( - color: Colors.white, - fontSize: 15, - ), - ), - ), - const SizedBox(height: 5), - ElevatedButton( - onPressed: () { - AuthenticationRepository.instance.logout(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - minimumSize: const Size(230, 40), - ), - child: const Text( - 'Cerrar Sesión', - style: TextStyle( - color: Colors.white, - fontSize: 15, - ), - ), - ), - const SizedBox(height: 5), - ], - ), - ), - ); - } -} diff --git a/lib/src/components/photo_view.dart b/lib/src/components/photo_view.dart deleted file mode 100644 index 37a26dd..0000000 --- a/lib/src/components/photo_view.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -const double photoSize = 100; -const double iconSize = 35; - -class ReferencePhoto extends StatelessWidget { - /// Accepts either a String URL or null. - final Object? ref; - final double size; - final double sizeIcon; - final double sizeCircle; - - const ReferencePhoto({ - super.key, - required this.ref, - this.sizeIcon = iconSize, - this.size = photoSize, - this.sizeCircle = photoSize, - }); - - @override - Widget build(BuildContext context) { - final url = ref is String ? ref as String : null; - - if (url == null || url.isEmpty || url.startsWith('...')) { - return DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon); - } - - return ClipOval( - child: Image.network( - url, - width: size, - height: size, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon), - ), - ); - } -} - -class DefaultPhoto extends StatelessWidget { - final double sizeDefault; - final double iconDefault; - - const DefaultPhoto({ - super.key, - this.sizeDefault = photoSize, - this.iconDefault = iconSize, - }); - - @override - Widget build(BuildContext context) { - return Container( - width: sizeDefault, - height: sizeDefault, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: Icon( - Icons.person, - color: const Color.fromARGB(255, 255, 255, 255), - size: iconDefault, - ), - ); - } -} - -class LocalPhoto extends StatelessWidget { - final File file; - - const LocalPhoto({super.key, required this.file}); - - @override - Widget build(BuildContext context) { - if (kIsWeb) { - return ClipOval( - child: Image.network( - file.path, - width: photoSize, - height: photoSize, - fit: BoxFit.cover, - ), - ); - } - - return ClipOval( - child: Image.file( - file, - width: photoSize, - height: photoSize, - fit: BoxFit.cover, - ), - ); - } -} diff --git a/lib/src/components/photo_view_web.dart b/lib/src/components/photo_view_web.dart deleted file mode 100644 index ccac4b2..0000000 --- a/lib/src/components/photo_view_web.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter/material.dart'; - -const double photoSize = 100; -const double iconSize = 55; - -class ReferencePhotoWeb extends StatelessWidget { - /// Accepts either a String URL or null. - final Object? ref; - final double size; - final double sizeIcon; - final double sizeCircle; - - const ReferencePhotoWeb({ - super.key, - required this.ref, - this.sizeIcon = iconSize, - this.size = photoSize, - this.sizeCircle = photoSize, - }); - - @override - Widget build(BuildContext context) { - final url = ref is String ? ref as String : null; - - if (url == null || url.isEmpty || url.startsWith('...')) { - return DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon); - } - - return ClipOval( - child: Image.network( - url, - width: size, - height: size, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon), - ), - ); - } -} - -class DefaultPhotoWeb extends StatelessWidget { - final double sizeDefault; - final double iconDefault; - - const DefaultPhotoWeb({ - super.key, - this.sizeDefault = photoSize, - this.iconDefault = iconSize, - }); - - @override - Widget build(BuildContext context) { - return Container( - width: sizeDefault, - height: sizeDefault, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: Icon( - Icons.person, - color: const Color.fromARGB(255, 255, 255, 255), - size: iconDefault, - ), - ); - } -} - -class LocalPhotoWeb extends StatelessWidget { - final Uint8List? file; - - const LocalPhotoWeb({super.key, required this.file}); - - @override - Widget build(BuildContext context) { - if (file == null) { - return DefaultPhotoWeb(); - } else { - return ClipOval( - child: Image.memory( - file!, - width: photoSize, - height: photoSize, - fit: BoxFit.cover, - ), - ); - } - } -} diff --git a/lib/src/components/pop_appbar.dart b/lib/src/components/pop_appbar.dart deleted file mode 100644 index 338c93b..0000000 --- a/lib/src/components/pop_appbar.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; - -class PopAppbar extends StatelessWidget implements PreferredSizeWidget { - final VoidCallback onPressed; - final String label; - - const PopAppbar({ - super.key, - required this.onPressed, - required this.label, - }); - - @override - Size get preferredSize => Size.fromHeight(kToolbarHeight); - - @override - Widget build(BuildContext context) { - return AppBar( - backgroundColor: Colors.white, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: onPressed, - ), - iconTheme: const IconThemeData( - color: Colors.black, - ), - title: Text( - label, - style: const TextStyle( - color: Colors.black, - ), - ), - ); - } -} diff --git a/lib/src/components/primary_btn.dart b/lib/src/components/primary_btn.dart deleted file mode 100644 index 7197924..0000000 --- a/lib/src/components/primary_btn.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -class PrimaryButtom extends StatelessWidget { - final VoidCallback onPressed; - final String label; - final bool - isEnabled; // Nuevo parámetro para indicar si el botón está habilitado - - const PrimaryButtom({ - Key? key, - required this.onPressed, - required this.label, - this.isEnabled = true, // Valor predeterminado: habilitado - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: isEnabled - ? onPressed - : null, // Habilita/deshabilita el botón según isEnabled - style: ElevatedButton.styleFrom( - backgroundColor: isEnabled - ? const Color(0xFF2BA4EC) - : Colors.grey, // Cambia el color de fondo - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: isEnabled - ? 0 - : 0, // Cambia la elevación para dar una sensación de clickeabilidad - minimumSize: const Size(230, 60), - ), - child: Text( - label, - style: TextStyle( - color: isEnabled - ? Colors.white - : Colors.black, // Cambia el color del texto - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ); - } -} diff --git a/lib/src/components/schedule_picker.dart b/lib/src/components/schedule_picker.dart deleted file mode 100644 index db033aa..0000000 --- a/lib/src/components/schedule_picker.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -typedef TimeCallback = void Function(TimeOfDay? pickedTime); - -class SchedulePicker extends StatefulWidget { - final String name; - Schedule schedule; - - SchedulePicker({super.key, required this.name, required this.schedule}); - - @override - State createState() => _SchedulePickerState(); -} - -class _SchedulePickerState extends State { - @override - Widget build(BuildContext context) { - return Column( - children: [ - customSwitch(widget.name, widget.schedule.habilitado, (value) { - widget.schedule.habilitado = value; - }), - ...datePickers(widget.schedule.habilitado), - const Divider( - height: 15, - color: Colors.grey, - ), - ], - ); - } - - List datePickers(bool value) { - if (!value) return []; - return [ - customSwitch( - 'Jornada continua', - widget.schedule.jornadaContinua, - (value) { - widget.schedule.jornadaContinua = value; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 25), - child: Row(children: [ - datePicker(widget.schedule.range1Hour1, - (pickedTime) => widget.schedule.range1Hour1 = pickedTime), - const Text('-'), - ...(!widget.schedule.jornadaContinua - ? [ - datePicker(widget.schedule.range1Hour2, - (pickedTime) => widget.schedule.range1Hour2 = pickedTime), - const Text(' '), - ] - : []), - ...(!widget.schedule.jornadaContinua - ? [ - datePicker(widget.schedule.range2Hour1, - (pickedTime) => widget.schedule.range2Hour1 = pickedTime), - const Text('-'), - ] - : []), - datePicker(widget.schedule.range2Hour2, - (pickedTime) => widget.schedule.range2Hour2 = pickedTime), - ]), - ), - ]; - } - - datePicker(TimeOfDay? time, TimeCallback callback) { - return Expanded( - child: TextFormField( - textAlign: TextAlign.center, - onTap: () async { - final TimeOfDay? pickedTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (pickedTime != null) { - setState(() { - callback(pickedTime); - }); - } - }, - readOnly: true, - decoration: const InputDecoration( - hintText: 'Hora', - ), - controller: TextEditingController( - text: time == null ? '' : time.format(context), - ), - style: const TextStyle(fontSize: 15), - ), - ); - } - - customSwitch(String text, bool switchValue, ValueChanged onChanged) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: SizedBox( - height: 40, - child: Row( - children: [ - Expanded( - child: Text( - text, - style: const TextStyle( - fontSize: 15, - color: Colors.black, - ), - )), - Transform.scale( - scale: 1.2, - child: Switch( - value: switchValue, - onChanged: (bool newValue) { - setState(() { - onChanged(newValue); - }); - }, - ), - ), - ], - ), - ), - ); - } -} - -class Schedule { - bool habilitado; - bool jornadaContinua; - TimeOfDay? range1Hour1; - TimeOfDay? range1Hour2; - TimeOfDay? range2Hour1; - TimeOfDay? range2Hour2; - - Schedule( - this.habilitado, - this.jornadaContinua, - this.range1Hour1, - this.range1Hour2, - this.range2Hour1, - this.range2Hour2, - ); - - static Schedule fromJson(Map json) { - return Schedule( - json['habilitado'], - json['jornadaContinua'], - _parseTime(json['range1Hour1']), - _parseTime(json['range1Hour2']), - _parseTime(json['range2Hour1']), - _parseTime(json['range2Hour2']), - ); - } - - static TimeOfDay? _parseTime(String? time) { - if (time == null) return null; - final components = time.split(' '); - final hourMinutes = components[0].split(':'); - final hour = int.parse(hourMinutes[0]); - final minutes = int.parse(hourMinutes[1]); - if (components[1] == 'PM' && hour < 12) { - return TimeOfDay(hour: hour + 12, minute: minutes); - } else if (components[1] == 'AM' && hour == 12) { - return TimeOfDay(hour: 0, minute: minutes); - } - return TimeOfDay(hour: hour, minute: minutes); - } - - static TimeOfDay stringToTimeOfDay(String? tod) { - if (tod == null) { - return TimeOfDay.now(); - } - final format = DateFormat.jm(); - return TimeOfDay.fromDateTime(format.parse(tod)); - } - - @override - String toString() { - return 'Schedule(habilitado: $habilitado, jornadaContinua: $jornadaContinua, ' - 'range1Hour1: $range1Hour1, range1Hour2: $range1Hour2, ' - 'range2Hour1: $range2Hour1, range2Hour2: $range2Hour2)'; - } - - static Map _emptySchedules() => { - for (var i = 1; i <= 7; i++) - '$i': Schedule(false, false, null, null, null, null), - }; - - static Future> getHorarios(String uid) async { - try { - final Map data = - await ApiService.instance.get('/users/$uid'); - final Map? horarioData = - data['horario'] as Map?; - if (horarioData == null) return _emptySchedules(); - final horarios = {}; - horarioData.forEach((key, value) { - horarios[key] = Schedule.fromJson(value as Map); - }); - return horarios; - } catch (e) { - print('Error getting horarios: $e'); - return _emptySchedules(); - } - } -} diff --git a/lib/src/controllers/add_name_email_city.dart b/lib/src/controllers/add_name_email_city.dart deleted file mode 100644 index 00b5656..0000000 --- a/lib/src/controllers/add_name_email_city.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; - -class NameEmailCityController { - static final instance = NameEmailCityController(); - - final name = TextEditingController(); - final email = TextEditingController(); - final city = TextEditingController(); -} diff --git a/lib/src/controllers/info_ professional.dart b/lib/src/controllers/info_ professional.dart deleted file mode 100644 index 967229a..0000000 --- a/lib/src/controllers/info_ professional.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; - -class InforProfessionalController { - static final instance = InforProfessionalController(); - - final cedula = TextEditingController(); - final profesion = TextEditingController(); - final especializacion = TextEditingController(); -} diff --git a/lib/src/controllers/login_email_controller.dart b/lib/src/controllers/login_email_controller.dart deleted file mode 100644 index 689ad46..0000000 --- a/lib/src/controllers/login_email_controller.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; - -class LoginEmailController { - static final instance = LoginEmailController(); - - final email = TextEditingController(); - final password = TextEditingController(); - - Future loginUser(String email, String password) async { - await AuthenticationRepository.instance.loginWithEmailAndPassword(email, password); - } -} diff --git a/lib/src/controllers/new_phone_controller.dart b/lib/src/controllers/new_phone_controller.dart deleted file mode 100644 index 0ff1f10..0000000 --- a/lib/src/controllers/new_phone_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; - -class NewPhoneController { - static final instance = NewPhoneController(); - - final newPhoneNo = TextEditingController(text: ''); - final otpCode = TextEditingController(text: ''); - - Future updatePhoneNumber(String newPhone) async { - try { - await ApiService.instance.patch('/users/me', {'phone': newPhone}); - showAppSnackBar('Número actualizado', 'El número de teléfono se actualizó correctamente.', - color: const Color(0xFF2BA4EC)); - } catch (e) { - showAppSnackBar('Error al actualizar', 'No se pudo actualizar el número: $e'); - } - } -} diff --git a/lib/src/controllers/otp_controller.dart b/lib/src/controllers/otp_controller.dart deleted file mode 100644 index 1bd1a64..0000000 --- a/lib/src/controllers/otp_controller.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:prosappco/src/presentation/screens/map/service.dart'; -import 'package:prosappco/src/presentation/screens/service_web.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; - -class OTPController { - static final instance = OTPController(); - - Future verifyOTP(String otp) async { - // OTP verification handled server-side; navigate home on completion - kIsWeb ? pushOffAll(const ServiceWebScreen()) : pushOffAll(const ServiceScreen()); - } -} diff --git a/lib/src/controllers/phone_auth_controller.dart b/lib/src/controllers/phone_auth_controller.dart deleted file mode 100644 index 33c1c1a..0000000 --- a/lib/src/controllers/phone_auth_controller.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; - -class PhoneAuthController { - static final instance = PhoneAuthController(); - - final phoneNo = TextEditingController(); - - Future phoneAuthentication(String phoneNo) async { - await AuthenticationRepository.instance.phoneAuthentication(phoneNo); - } -} diff --git a/lib/src/controllers/register_controller.dart b/lib/src/controllers/register_controller.dart deleted file mode 100644 index 1cb8519..0000000 --- a/lib/src/controllers/register_controller.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; - -class RegisterController { - static final instance = RegisterController(); - - final email = TextEditingController(); - final password = TextEditingController(); - final name = TextEditingController(); - - Future registerUser(String email, String password, String name) async { - await AuthenticationRepository.instance.createUserWithEmailAndPassword(email, password, name); - } -} diff --git a/lib/src/models/chat_model.dart b/lib/src/models/chat_model.dart deleted file mode 100644 index b121061..0000000 --- a/lib/src/models/chat_model.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class ChatModel { - List messages; - String professional_id; - String user_id; - UserModel? user; - UserModel? professional; - String id; - - ChatModel({ - required this.messages, - required this.professional_id, - required this.user_id, - this.user, - this.professional, - required this.id, - }); - - static ChatModel fromJson(Map data) { - final List msgs = data['messages'] ?? data['message'] ?? []; - final messages = msgs.map((m) { - return MessageModel( - user: m['user'] ?? m['sender_id'] ?? '', - content: m['content'] ?? m['text'] ?? '', - timestamp: m['timestamp'] != null - ? DateTime.tryParse(m['timestamp'].toString()) ?? DateTime.now() - : DateTime.now(), - ); - }).toList(); - - return ChatModel( - messages: messages, - professional_id: data['professional_id'] ?? '', - user_id: data['user_id'] ?? '', - id: data['id']?.toString() ?? '', - ); - } - - // Both roles use GET /chat/my — JWT identifies the current user - static Future> getChatsByProId(String userId) => - _getMyChats(); - - static Future> getChatsByUserId(String userId) => - _getMyChats(); - - static Future> _getMyChats() async { - try { - final data = await ApiService.instance.get('/chat/my'); - final List raw = data is List ? data : (data['data'] ?? []); - return raw - .map((e) => ChatModel.fromJson(e as Map)) - .toList(); - } catch (e) { - print('error getMyChats $e'); - return []; - } - } -} - -class MessageModel { - String user; - String content; - DateTime timestamp; - - MessageModel( - {required this.user, required this.content, required this.timestamp}); -} diff --git a/lib/src/models/event_model.dart b/lib/src/models/event_model.dart deleted file mode 100644 index ca0b1d2..0000000 --- a/lib/src/models/event_model.dart +++ /dev/null @@ -1,266 +0,0 @@ -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -final uid = AuthenticationRepository.instance.getCurrentUserUid(); - -// Backend status strings -const _statusMap = { - 'pendiente': 'pending', - 'aprobado': 'accepted', - 'negado': 'denied', - 'activo': 'active', - 'cancelado': 'cancelled', - 'completado': 'completed', -}; - -class EventoService { - Future createEvent( - String title, - String description, - String day, - String range1Hour1, - String range1Hour2, - String professionalId, - String ubicacion, - String address, - double latitude, - double longitude, - String status, - int? tarifa, - bool professionalScored, - bool userScored, - ) async { - try { - String fmt(String t) { - // Normalize "8:30 AM" / "08:30" → "HH:MM" - try { - final parts = t.replaceAll(RegExp(r'[APM ]'), '').split(':'); - final h = int.parse(parts[0]).toString().padLeft(2, '0'); - final m = (parts.length > 1 ? int.parse(parts[1]) : 0) - .toString() - .padLeft(2, '0'); - return '$h:$m'; - } catch (_) { - return '00:00'; - } - } - - final Map result = - await ApiService.instance.post('/services', { - 'professional_id': professionalId, - 'day': day, - 'description': description, - 'rate': tarifa ?? 0, - 'range1_hour1': fmt(range1Hour1), - 'range1_hour2': fmt(range1Hour2), - 'address': address, - 'latitude': latitude, - 'longitude': longitude, - 'location_preference': ubicacion == 'domicilio' ? 'delivery' : 'office', - }); - return result['id']?.toString(); - } catch (e) { - print('Evento $e'); - return null; - } - } -} - -// Calendar for logged-in professional — uses JWT, ignores day filter on backend -Future> getByProId(String day) async { - try { - final data = await ApiService.instance.get('/services/professional/calendar'); - final List raw = data is List ? data : (data['data'] ?? []); - return raw - .map((e) => Event.fromJson(e as Map)) - .where((e) => e.day.startsWith(day.substring(0, 10))) - .toList(); - } catch (e) { - print('Error getByProId $e'); - return []; - } -} - -// Active/accepted services for logged-in professional -Future> getByProIdAll(String state1, String state2) async { - try { - final data = await ApiService.instance.get('/services/professional'); - final List raw = data is Map ? (data['data'] ?? []) : (data as List); - final allowed = { - _statusMap[state1] ?? state1, - _statusMap[state2] ?? state2, - }; - return raw - .map((e) => Event.fromJson(e as Map)) - .where((e) => allowed.contains(e.status)) - .toList(); - } catch (e) { - print('Error getByProIdAll $e'); - return []; - } -} - -// Active/accepted services for logged-in user -Future> getByUserIdAll(String state1, String state2) async { - try { - final data = await ApiService.instance.get('/services/me'); - final List raw = data is Map ? (data['data'] ?? []) : (data as List); - final allowed = { - _statusMap[state1] ?? state1, - _statusMap[state2] ?? state2, - }; - return raw - .map((e) => Event.fromJson(e as Map)) - .where((e) => allowed.contains(e.status)) - .toList(); - } catch (e) { - print('Error getByUserIdAll $e'); - return []; - } -} - -class Event { - String? id; - String title; - String? description; - String day; - String range1Hour1; - String? range1Hour2; - String userId; - String professionalId; - String? ubicacion; - String? address; - double? longitud; - double? latitud; - String status; - ScoresModel? scoresModel; - int? tarifa; - bool professionalScored; - bool userScored; - DateTime? timeStamp; - - Event({ - this.id, - required this.title, - this.description, - required this.day, - required this.range1Hour1, - this.range1Hour2, - required this.userId, - required this.professionalId, - this.ubicacion, - this.address, - this.longitud, - this.latitud, - this.status = 'pending', - this.timeStamp, - this.tarifa, - this.professionalScored = false, - this.userScored = false, - }); - - factory Event.fromJson(Map json) { - DateTime? ts; - final raw = json['created_at'] ?? json['Timestamp']; - if (raw is String) ts = DateTime.tryParse(raw); - - // Backend sends ISO date for range times; extract HH:MM - String parseTime(dynamic v) { - if (v == null) return ''; - final s = v.toString(); - if (s.contains('T')) { - final dt = DateTime.tryParse(s); - if (dt != null) { - return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; - } - } - return s; - } - - // Nested professional user_id takes priority as professionalId - final nestedPro = json['professionals']; - final professionalId = (nestedPro is Map) - ? (nestedPro['user_id']?.toString() ?? json['professional_id']?.toString() ?? '') - : json['professional_id']?.toString() ?? ''; - - return Event( - id: json['id']?.toString() ?? '', - title: json['description'] ?? json['title'] ?? '', - description: json['description'], - day: json['day'] ?? '', - range1Hour1: parseTime(json['range1_hour1'] ?? json['range1Hour1']), - range1Hour2: parseTime(json['range1_hour2'] ?? json['range1Hour2']), - userId: json['user_id'] ?? '', - professionalId: professionalId, - ubicacion: json['location_preference'] ?? json['ubicacion'] ?? '', - address: json['address'] ?? '', - longitud: (json['longitude'] ?? 0).toDouble(), - latitud: (json['latitude'] ?? 0).toDouble(), - status: json['status'] ?? 'pending', - timeStamp: ts, - tarifa: (json['rate'] as num?)?.toInt() ?? json['tarifa'] ?? 0, - professionalScored: json['professional_scored'] ?? false, - userScored: json['user_scored'] ?? false, - ); - } - - static Future getEventById(String eventId) async { - try { - final Map data = - await ApiService.instance.get('/services/$eventId'); - return Event.fromJson(data); - } catch (e) { - print('Error getting event: $e'); - return Event( - title: '', day: '', range1Hour1: '', userId: '', professionalId: ''); - } - } - - // All services for a given professional (by user_id) — uses public calendar - static Future> getEventsAllById(String proId) async { - try { - final data = - await ApiService.instance.get('/services/public-calendar/$proId'); - final List raw = (data is Map ? data['services'] : data) ?? []; - return raw - .map((e) => Event.fromJson(e as Map)) - .toList(); - } catch (e) { - print('Error getEventsAllById $e'); - return []; - } - } - - static Future> getEventsAllByIdAndStatus(String proId) async { - try { - final data = - await ApiService.instance.get('/services/public-calendar/$proId'); - final List raw = (data is Map ? data['services'] : data) ?? []; - return raw - .map((e) => Event.fromJson(e as Map)) - .where((e) => e.status == 'accepted' || e.status == 'pending') - .toList(); - } catch (e) { - print('Error getEventsAllByIdAndStatus $e'); - return []; - } - } - - static Future> getEventsAllByIdStatus( - String proId, String state) async { - try { - final backendStatus = _statusMap[state] ?? state; - final data = - await ApiService.instance.get('/services/professional/calendar'); - final List raw = data is List ? data : []; - return raw - .map((e) => Event.fromJson(e as Map)) - .where((e) => e.status == backendStatus) - .toList(); - } catch (e) { - print('Error getEventsAllByIdStatus $e'); - return []; - } - } -} diff --git a/lib/src/models/professional_model.dart b/lib/src/models/professional_model.dart deleted file mode 100644 index d52c9b7..0000000 --- a/lib/src/models/professional_model.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class Professional { - final String id; - final String? photoUrl; - final String name; - final String professionName; - final String cityName; - final String ubicacion; - final String realAddress; - final double latitude; - final double longitude; - final List professionalEspecializado; - final ScoresModel scores; - final int? tarifa; - final String? token; - - Professional({ - required this.id, - this.photoUrl, - required this.name, - required this.professionName, - required this.cityName, - required this.ubicacion, - required this.professionalEspecializado, - required this.scores, - required this.realAddress, - required this.latitude, - required this.longitude, - this.tarifa, - this.token, - }); - - String getEspecializaciones() { - return professionalEspecializado.join(',\n'); - } - - @override - String toString() { - return 'Professional { photoUrl: $photoUrl, name: $name, professionName: $professionName, cityName: $cityName }'; - } - - static Future getProfessional(String uid) async { - try { - final Map data = - await ApiService.instance.get('/professionals/$uid'); - - final List especializaciones = - ((data['especialidades'] ?? data['specialties'] ?? []) as List) - .map((e) => e.toString()) - .toList(); - - return Professional( - id: uid, - photoUrl: data['picture'] ?? data['photo_url'], - name: data['name'] ?? '', - professionName: data['profession'] ?? data['profesion'] ?? '', - cityName: data['city'] ?? '', - professionalEspecializado: especializaciones, - ubicacion: data['ubicacion'] ?? '', - realAddress: data['address'] ?? '', - latitude: (data['latitude'] ?? 0).toDouble(), - longitude: (data['longitude'] ?? 0).toDouble(), - scores: await ScoresModel.scoreTo(uid, true, false), - tarifa: data['rate'] != null - ? double.tryParse(data['rate'].toString())?.toInt() - : data['tarifas'], - token: data['token'], - ); - } catch (e) { - print('Error al obtener profesional: $e'); - return null; - } - } -} diff --git a/lib/src/models/scores_model.dart b/lib/src/models/scores_model.dart deleted file mode 100644 index 5ccd80c..0000000 --- a/lib/src/models/scores_model.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:prosappco/src/services/api_service.dart'; - -class ScoresModel { - late int total; - late double average; - final List details; - - ScoresModel(this.details) { - total = details.length; - average = averageScore(details); - } - - @override - String toString() { - return 'ScoresModel{total: $total, average: $average, details: $details}'; - } - - double averageScore(List details) { - if (details.isEmpty) return 0.0; - final sum = details.map((d) => d.score).reduce((a, b) => a + b); - return sum / details.length; - } - - static Future scoreTo( - String? userId, bool isFromClient, bool userInfo) async { - if (userId == null || userId.isEmpty) return ScoresModel([]); - try { - final List data = - await ApiService.instance.get('/comments/reputation/$userId'); - final details = - data.map((e) => ScoreDetailModel.fromJson(e as Map)).toList(); - return ScoresModel(details); - } catch (e) { - print('Error scoreTo: $e'); - return ScoresModel([]); - } - } - - // ponytail: scoreFrom and scoreTo now both hit the same reputation endpoint - static Future scoreFrom( - String userId, bool isFromClient, bool userInfo) async { - return scoreTo(userId, isFromClient, userInfo); - } -} - -class ScoreDetailModel { - final String id; - final double score; - final String fromUser; - final String toUser; - final String comment; - final bool isFromClient; - final String name; - final String? avatar; - - ScoreDetailModel({ - required this.id, - required this.score, - required this.fromUser, - required this.toUser, - required this.comment, - required this.isFromClient, - required this.name, - this.avatar, - }); - - factory ScoreDetailModel.fromJson(Map data) { - return ScoreDetailModel( - id: data['id']?.toString() ?? '', - score: double.tryParse(data['score']?.toString() ?? '0') ?? 0, - fromUser: data['from_user'] ?? '', - toUser: data['to_user'] ?? '', - comment: data['comment'] ?? '', - isFromClient: data['is_from_professional'] ?? false, - name: data['from_user_name'] ?? data['name'] ?? '...', - avatar: data['avatar'], - ); - } - - @override - String toString() { - return 'ScoreDetailModel{id: $id, score: $score, fromUser: $fromUser, name: $name}'; - } -} diff --git a/lib/src/models/setting_model.dart b/lib/src/models/setting_model.dart deleted file mode 100644 index 25144e4..0000000 --- a/lib/src/models/setting_model.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:prosappco/src/services/api_service.dart'; - -class SettingModel { - final bool domicilios; - final bool google; - final bool tarifas; - final String titulo; - final String parrafo; - final String numero; - final String email; - final String dias; - final String horas; - final String version; - final String proliticasPrivacidad; - final String terminosCondiciones; - - SettingModel( - this.domicilios, - this.google, - this.tarifas, - this.titulo, - this.parrafo, - this.numero, - this.email, - this.dias, - this.horas, - this.version, - this.proliticasPrivacidad, - this.terminosCondiciones, - ); - - static SettingModel _empty() => - SettingModel(false, false, false, '', '', '', '', '', '', '', '', ''); - - static Future fromJson(Map? json) async { - if (json == null) return _empty(); - try { - return SettingModel( - json['domicilios'] ?? false, - json['google'] ?? false, - json['tarifas'] ?? false, - json['titulo_soporte'] ?? '', - json['parrafo_soporte'] ?? '', - json['numero_soporte'] ?? '', - json['email_soporte'] ?? '', - json['dias_soporte'] ?? '', - json['horas_soporte'] ?? '', - json['version'] ?? '', - json['politicas_privacidad'] ?? '', - json['terminos_condiciones'] ?? '', - ); - } catch (e) { - print('Error settings: $e'); - return _empty(); - } - } - - static Future getSettings() async { - try { - final Map data = - await ApiService.instance.get('/settings'); - return fromJson(data); - } catch (e) { - print('Error getting settings: $e'); - return _empty(); - } - } - - @override - String toString() { - return 'SettingModel { domicilios: $domicilios, google: $google, tarifas: $tarifas, ' - 'titulo: $titulo, parrafo: $parrafo, numero: $numero, ' - 'email: $email, dias: $dias, horas: $horas, ' - 'version: $version, politicasPrivacidad: $proliticasPrivacidad, ' - 'terminosCondiciones: $terminosCondiciones }'; - } -} diff --git a/lib/src/models/user_model.dart b/lib/src/models/user_model.dart deleted file mode 100644 index 4a21c70..0000000 --- a/lib/src/models/user_model.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:prosappco/src/services/api_service.dart'; - -class UserModel { - final String id; - final String name; - final String city; - final String? profession; - final int proState; - final String? picture; - final String? banner; - final int? tarifa; - final String? phoneNumber; - final String? email; - final String? gender; - final String? birthday; - - UserModel({ - required this.id, - required this.name, - required this.city, - this.profession, - this.proState = 0, - this.picture, - this.banner, - this.tarifa, - this.phoneNumber, - this.email, - this.gender, - this.birthday, - }); - - factory UserModel.fromApi(Map json) { - final professional = json['professionals'] as Map?; - return UserModel( - id: json['id'] ?? '', - name: json['name'] ?? 'Sin nombre', - city: json['city'] ?? '', - profession: professional?['profession'], - proState: json['pro_state'] ?? 0, - picture: json['picture'], - banner: professional?['banner_picture'], - tarifa: professional?['rate'] != null - ? double.tryParse(professional!['rate'].toString())?.toInt() - : null, - phoneNumber: json['phone'], - email: json['email'], - gender: json['gender'], - birthday: json['birthday'], - ); - } - - static Future getUser(String id) async { - try { - final data = await ApiService.instance.get('/users/$id'); - return UserModel.fromApi(data as Map); - } catch (_) { - return null; - } - } - - @override - String toString() => - 'UserModel(id: $id, name: $name, city: $city, profession: $profession, proState: $proState)'; -} diff --git a/lib/src/presentation/screens/about.dart b/lib/src/presentation/screens/about.dart deleted file mode 100644 index 4bf1e71..0000000 --- a/lib/src/presentation/screens/about.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/presentation/screens/web_view.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class AboutScreen extends StatefulWidget { - const AboutScreen({super.key}); - - @override - State createState() => _AboutScreenState(); -} - -class _AboutScreenState extends State { - SettingModel? settings; - - @override - void initState() { - super.initState(); - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - }), - ); - } - } - - void _launchURL(String url) async { - if (await canLaunch(url)) { - await launch(url, forceSafariVC: false, forceWebView: false); - } else { - throw 'No se pudo abrir el enlace $url'; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Acerca de la aplicación'), - body: ListView( - children: [ - ListTile( - onTap: () { - if (kIsWeb) { - _launchURL(settings?.proliticasPrivacidad ?? ''); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Políticas de privacidad', - link: settings?.proliticasPrivacidad ?? '', - ); - }, - ), - ); - } - }, - title: const Text('Políticas de privacidad'), - trailing: - const Icon(Icons.keyboard_arrow_right, color: Colors.black), - ), - ListTile( - onTap: () { - if (kIsWeb) { - _launchURL(settings?.terminosCondiciones ?? ''); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Términos y condiciones', - link: settings?.terminosCondiciones ?? '', - ); - }, - ), - ); - } - }, - title: const Text('Términos y condiciones'), - trailing: - const Icon(Icons.keyboard_arrow_right, color: Colors.black), - ), - ListTile( - title: const Text('Versión de la aplicación'), - subtitle: Text(settings?.version ?? ''), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/calendar.dart b/lib/src/presentation/screens/calendar.dart deleted file mode 100644 index 69daf47..0000000 --- a/lib/src/presentation/screens/calendar.dart +++ /dev/null @@ -1,496 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/presentation/screens/cita.dart'; -import 'package:prosappco/src/presentation/widgets/shared/loading_item_list.dart'; -import 'package:table_calendar/table_calendar.dart'; -import 'package:intl/intl.dart'; - -class CalendarScreen extends StatefulWidget { - const CalendarScreen({super.key}); - - @override - State createState() => _CalendarScreenState(); -} - -class _CalendarScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List? _events; - - final _titleController = TextEditingController(); - final _descriptionController = TextEditingController(); - - EventoService eventoService = EventoService(); - CalendarFormat _calendarFormat = CalendarFormat.month; - - DateTime today = DateTime.now(); - DateTime now = DateTime.now(); - - TimeOfDay? _selectedTime1; - TimeOfDay? _selectedTime2; - - Future _selectTime1(BuildContext context) async { - final TimeOfDay? pickedTime1 = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (pickedTime1 != null) { - setState(() { - _selectedTime1 = pickedTime1; - }); - } - return pickedTime1; - } - - Future _selectTime2(BuildContext context) async { - final TimeOfDay? pickedTime2 = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (pickedTime2 != null) { - setState(() { - _selectedTime2 = pickedTime2; - }); - } - - return pickedTime2; - } - - @override - void initState() { - super.initState(); - - today = DateTime.utc(today.year, today.month, today.day); - - if (_events == null) { - Event.getEventsAllByIdStatus(uid ?? "", 'aprobado') - .then((value) => setState(() { - _events = value; - })); - } - } - - void _onDaySelected(DateTime day, DateTime focusedDay) { - setState(() { - today = day; - }); - } - - void _onFormatChange(CalendarFormat format) { - setState(() { - _calendarFormat = format; - }); - } - - @override - Widget build(BuildContext context) { - if (_events == null) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); - } - - var events = _events!; - // DateTime firstDay = today.subtract(Duration(days: 365)); - DateTime lastDay = today.add(const Duration(days: 365)); - - return Scaffold( - floatingActionButtonLocation: kIsWeb - ? FloatingActionButtonLocation.startFloat - : FloatingActionButtonLocation.endFloat, - resizeToAvoidBottomInset: false, - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Calendario', - ), - body: Column( - children: [ - Container( - color: const Color.fromARGB(255, 224, 247, 255), - child: TableCalendar( - locale: 'es_MX', - firstDay: DateTime.utc(2010, 10, 16), - lastDay: lastDay, - focusedDay: today, - availableGestures: AvailableGestures.all, - onDaySelected: _onDaySelected, - selectedDayPredicate: (day) => isSameDay(day, today), - calendarFormat: _calendarFormat, - onFormatChanged: _onFormatChange, - eventLoader: (date) { - return events - .where((element) { - DateTime day = DateTime.parse(element.day); - return (date.year == day.year && - date.month == day.month && - date.day == day.day); - }) - .map((e) => e.description) - .toList(); - }, - availableCalendarFormats: const { - CalendarFormat.month: 'Mes', - CalendarFormat.week: 'Semana', - CalendarFormat.twoWeeks: '2 Semanas', - }, - ), - ), - SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - child: Text(DateFormat('dd MMMM yyyy', 'es').format(today), - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, - )), - ), - ), - const Divider( - height: 0, - ), - Expanded(child: SingleChildScrollView(child: _eventList())) - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: _showDialog, - child: const Icon(Icons.add), - ), - ); - } - - void _showDialog() { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - content: StatefulBuilder( - builder: (BuildContext context, StateSetter setStateDialog) { - return SizedBox( - height: 800, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 0, vertical: 15), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - _selectedTime1 == null - ? '' - : _selectedTime1!.format(context), - style: TextStyle( - color: Colors.grey[500], - fontSize: 12, - ), - ), - _selectedTime2 != null && _selectedTime1 != null - ? Text( - ' - ', - style: TextStyle( - color: Colors.grey[500], - fontSize: 12, - ), - ) - : const SizedBox(), - Text( - _selectedTime2 == null - ? '' - : _selectedTime2!.format(context), - style: TextStyle( - color: Colors.grey[500], - fontSize: 12, - ), - ), - Text( - ' | ', - style: TextStyle( - color: Colors.grey[500], - ), - ), - Text( - DateFormat('dd MMMM yyyy', 'es').format(today), - style: const TextStyle( - fontSize: 13, - ), - ) - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: TextFormField( - controller: _titleController, - decoration: const InputDecoration(hintText: 'Titulo'), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 20), - child: TextFormField( - controller: _descriptionController, - decoration: - const InputDecoration(hintText: 'Descripción'), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 40), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 80, - child: TextFormField( - textAlign: TextAlign.center, - onTap: () async { - var value = await _selectTime1(context); - setStateDialog(() { - _selectedTime1 = value; - }); - }, - readOnly: true, - decoration: const InputDecoration( - hintText: 'Hora', - ), - controller: TextEditingController( - text: _selectedTime1 == null - ? '' - : ' ${_selectedTime1!.format(context)}'), - style: const TextStyle(fontSize: 15), - ), - ), - const Text(' - '), - SizedBox( - width: 80, - child: TextFormField( - textAlign: TextAlign.center, - onTap: () async { - var value = await _selectTime2(context); - setStateDialog(() { - _selectedTime2 = value; - }); - }, - readOnly: true, - decoration: const InputDecoration( - hintText: 'Hora', - ), - controller: TextEditingController( - text: _selectedTime2 == null - ? '' - : ' ${_selectedTime2!.format(context)}'), - style: const TextStyle(fontSize: 15), - ), - ), - ], - ), - ), - PrimaryButtom( - onPressed: () async { - final DateTime combinedDate1 = DateTime( - today.year, - today.month, - today.day, - _selectedTime1!.hour, - _selectedTime1!.minute, - ); - final DateTime combinedDate2 = DateTime( - today.year, - today.month, - today.day, - _selectedTime2!.hour, - _selectedTime2!.minute, - ); - - await eventoService - .createEvent( - _titleController.text, - _descriptionController.text, - DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(today), - '$combinedDate1', - '$combinedDate2', - uid.toString(), - 'sitio', - '', - 0, - 0, - 'aprobado', - 0, - false, - false, - ) - .then((value) { - Navigator.pop(context); - _titleController.text = ''; - _descriptionController.text = ''; - }); - - Event.getEventsAllByIdStatus(uid ?? "", 'aprobado') - .then( - (value) => setState(() { - _events = value; - }), - ); - }, - label: 'Añadir evento', - ), - ], - ), - ); - }, - ), - ); - }, - ); - } - - Widget _eventList() { - return FutureBuilder( - future: getByProId(DateFormat("yyyy-MM-dd 00:00:00.000").format(today)), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - List eventos = []; - - if (snapshot.connectionState == ConnectionState.waiting) { - return const Column( - children: [ - LoadingItemList(useCircleAvatar: false), - LoadingItemList(useCircleAvatar: false), - LoadingItemList(useCircleAvatar: false), - LoadingItemList(useCircleAvatar: false), - LoadingItemList(useCircleAvatar: false), - ], - ); - } - try { - snapshot.data!.sort((a, b) { - String? range1Hour1A = a.range1Hour1; - String? range1Hour1B = b.range1Hour1; - - DateTime dateTimeA = DateTime.parse(range1Hour1A); - DateTime dateTimeB = DateTime.parse(range1Hour1B); - - return dateTimeB.compareTo(dateTimeA); - }); - - eventos.addAll(snapshot.data!); - } catch (e) { - print("Error al cargar eventos: inflar $e"); - } - - if (eventos.isEmpty) { - return const Padding( - padding: EdgeInsets.only(top: 30), - child: Center( - child: Text( - 'No tienes citas', - style: TextStyle( - color: Colors.black, - fontSize: - 18, // Tamaño de fuente ajustado según tus preferencias - fontWeight: - FontWeight.w500, // Puedes ajustar el peso de la fuente - fontStyle: FontStyle.italic, // Puedes agregar estilo italic - // Otros estilos según tus preferencias - ), - ), - ), - ); - } - return Column( - children: [ - ...eventos.map( - (e) => ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: e); - }, - ), - ); - }, - leading: Text( - TimeOfDay.fromDateTime(DateTime.parse(e.range1Hour1)) - .format(context)), - title: RichText( - text: TextSpan( - children: [ - TextSpan( - text: '${e.title}, ', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - TextSpan( - text: DateFormat('dd MMM', 'es') - .format(DateTime.parse(e.day)), - style: const TextStyle( - color: Colors.grey, - fontSize: 16, - ), - ), - ], - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - e.professionalId == e.userId - ? const SizedBox() - : RatingBar.builder( - initialRating: e.scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: - const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - e.professionalId == e.userId - ? const SizedBox() - : Text( - '(${e.scoresModel?.total.toString()}) ${e.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '" ${e.description} "', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - trailing: const Icon(Icons.keyboard_arrow_right), - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/src/presentation/screens/calendar_pro.dart b/lib/src/presentation/screens/calendar_pro.dart deleted file mode 100644 index 77627c7..0000000 --- a/lib/src/presentation/screens/calendar_pro.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/schedule_picker.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; -import 'package:prosappco/src/utils/time_of_day_utils.dart'; -import 'package:table_calendar/table_calendar.dart'; -import 'package:intl/intl.dart'; - -class CalendarProScreen extends StatefulWidget { - final Professional professional; - - const CalendarProScreen({super.key, required this.professional}); - - @override - State createState() => _CalendarProScreenState(); -} - -class _CalendarProScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - EventoService eventoService = EventoService(); - CalendarFormat _calendarFormat = CalendarFormat.month; - - DateTime today = DateTime.now(); - DateTime now = DateTime.now(); - late int numDay; - - List? _events; - - Map? _horarios; - - @override - void initState() { - super.initState(); - - if (_horarios == null) { - Schedule.getHorarios(widget.professional.id.toString()).then( - (Map data) { - setState(() { - _horarios = data; - }); - }, - ); - } - - if (_events == null) { - Event.getEventsAllByIdAndStatus(widget.professional.id.toString()) - .then((value) { - setState(() { - _events = value; - }); - }); - } - - today = DateTime.utc(today.year, today.month, today.day); - numDay = today.weekday; - } - - void _onDaySelected(DateTime day, DateTime focusedDay) { - setState(() { - today = day; - numDay = today.weekday; - }); - } - - void _onFormatChange(CalendarFormat format) { - setState(() { - _calendarFormat = format; - }); - } - - @override - Widget build(BuildContext context) { - if (_events == null) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); - } - - DateTime lastDay = today.add(const Duration(days: 365)); - - return Scaffold( - floatingActionButtonLocation: kIsWeb - ? FloatingActionButtonLocation.startFloat - : FloatingActionButtonLocation.endFloat, - resizeToAvoidBottomInset: false, - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Calendario', - ), - body: Column( - children: [ - Container( - color: const Color.fromARGB(255, 224, 247, 255), - child: TableCalendar( - locale: 'es_MX', - firstDay: DateTime.now(), - lastDay: lastDay, - focusedDay: today, - availableGestures: AvailableGestures.all, - onDaySelected: _onDaySelected, - selectedDayPredicate: (day) => isSameDay(day, today), - calendarFormat: _calendarFormat, - onFormatChanged: _onFormatChange, - availableCalendarFormats: const { - CalendarFormat.month: 'Mes', - CalendarFormat.week: 'Semana', - CalendarFormat.twoWeeks: '2 Semanas', - }, - ), - ), - SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - child: Text(DateFormat('dd MMMM yyyy', 'es').format(today), - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, - )), - ), - ), - const Divider( - height: 0, - ), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 15), - child: Column( - children: [...rangesItems(_horarios?[numDay.toString()])], - ), - )) - ], - ), - ); - } - - List rangesItems(Schedule? schedule) { - if (schedule == null) { - return const [ - Padding( - padding: EdgeInsets.only(top: 20, left: 30, right: 30), - child: Text( - 'El profesional no acepta turnos este día', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), - textAlign: TextAlign.center, - ), - ), - ]; - } - if (!schedule.habilitado) { - return const [ - Padding( - padding: EdgeInsets.only(top: 20, left: 30, right: 30), - child: Text( - 'El profesional no acepta turnos este día', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), - textAlign: TextAlign.center, - ), - ) - ]; - } - if (schedule.jornadaContinua) { - List ranges = TimeOfDayUtils.genRanges( - schedule.range1Hour1!, - schedule.range2Hour2!, - ); - - return rangesItemList(ranges, _events); - } else { - List ranges1 = TimeOfDayUtils.genRanges( - schedule.range1Hour1!, - schedule.range1Hour2!, - ); - List ranges2 = TimeOfDayUtils.genRanges( - schedule.range2Hour1!, - schedule.range2Hour2!, - ); - - return [ - ...rangesItemList(ranges1, _events), - ...rangesItemList(ranges2, _events), - ]; - } - } - - List rangesItemList(List ranges, List? events) { - return ranges.map((time) { - if (_isHora1Ocupada(time, events)) { - return Card( - elevation: 4, - margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: ListTile( - onTap: () { - WarningSnackbar.show( - title: 'Ocupado', - message: 'Este horário ya se encuentra ocupado', - ); - }, - contentPadding: const EdgeInsets.all(16), - leading: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.yellow, Colors.red, Colors.red], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - Icons.access_time, - color: Colors.white, - ), - ), - ), - title: Text( - time.format(context), - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), - ), - subtitle: const Text( - 'Ocupado', - style: TextStyle( - color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold), - ), - trailing: const Icon( - Icons.arrow_forward_ios, - color: Colors.grey, - ), - ), - ); - } else { - return Card( - elevation: 4, - margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: ListTile( - onTap: () { - Navigator.pop(context, [today, time, widget.professional]); - }, - contentPadding: const EdgeInsets.all(16), - leading: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.blue, Colors.green], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - Icons.access_time, - color: Colors.white, - ), - ), - ), - title: Text( - time.format(context), - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), - ), - subtitle: const Text( - 'Disponible', - style: TextStyle( - color: Colors.green, - fontSize: 13, - fontWeight: FontWeight.bold), - ), - trailing: const Icon( - Icons.arrow_forward_ios, - color: Colors.grey, - ), - ), - ); - } - }).toList(); - } - - bool _isHora1Ocupada(TimeOfDay hora1, List? events) { - if (events != null) { - for (Event event in events) { - DateTime time1 = DateTime( - today.year, - today.month, - today.day, - hora1.hour, - hora1.minute, - ); - - if (event.range1Hour1 == time1.toString()) { - return true; - } - } - } - return false; - } -} diff --git a/lib/src/presentation/screens/chat.dart b/lib/src/presentation/screens/chat.dart deleted file mode 100644 index f814950..0000000 --- a/lib/src/presentation/screens/chat.dart +++ /dev/null @@ -1,319 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/chat_model.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/presentation/screens/professional_info.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class ChatScreen extends StatefulWidget { - final String? eventoId; - const ChatScreen({super.key, this.eventoId}); - - @override - State createState() => _ChatScreenState(); -} - -class _ChatScreenState extends State { - final _textController = TextEditingController(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - // Other user info - String otherName = ''; - String? otherPhoto; - String? otherProfession; - Professional? professional; - bool pro = false; - - // Chat state - String? chatId; - List messages = []; - bool loading = true; - - @override - void initState() { - super.initState(); - _init(); - } - - Future _init() async { - try { - final event = await Event.getEventById(widget.eventoId!); - - final isClient = uid == event.userId; - final otherUserId = - isClient ? event.professionalId : event.userId; - pro = isClient; - - // Load other user info - final Map userData = - await ApiService.instance.get('/users/$otherUserId'); - if (mounted) { - setState(() { - otherName = userData['name'] ?? ''; - otherPhoto = userData['picture']; - otherProfession = userData['profession']; - }); - } - - if (isClient) { - professional = await Professional.getProfessional(event.professionalId); - } - - // Start/get chat - final Map chatData = await ApiService.instance - .post('/chat/start/${event.professionalId}', {}); - chatId = chatData['id']?.toString() ?? chatData['chatId']?.toString(); - - await _loadMessages(); - } catch (e) { - print('Error initializing chat: $e'); - if (mounted) setState(() => loading = false); - } - } - - Future _loadMessages() async { - if (chatId == null) return; - try { - final List data = - await ApiService.instance.get('/chat/$chatId/messages'); - if (mounted) { - setState(() { - messages = data.map((m) { - return MessageModel( - user: m['sender_id'] ?? m['user'] ?? '', - content: m['content'] ?? m['text'] ?? '', - timestamp: m['created_at'] != null - ? DateTime.tryParse(m['created_at'].toString()) ?? - DateTime.now() - : DateTime.now(), - ); - }).toList(); - loading = false; - }); - } - } catch (e) { - print('Error loading messages: $e'); - if (mounted) setState(() => loading = false); - } - } - - Future _sendMessage() async { - final text = _textController.text.trim(); - if (text.isEmpty || chatId == null) return; - _textController.clear(); - try { - await ApiService.instance - .post('/chat/$chatId/message', {'content': text}); - await _loadMessages(); - } catch (e) { - print('Error sending message: $e'); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Chat'), - body: Column( - children: [ - Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 3, - offset: const Offset(0, 2), - ), - ], - ), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - color: const Color(0xFFD6F4FF), - alignment: Alignment.topCenter, - child: ListTile( - leading: GestureDetector( - onTap: () { - if (pro && professional != null) { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return ProfessionalInfoScreen( - professional: professional!, - ); - }, - ), - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ReferencePhoto( - ref: otherPhoto, - size: 55, - sizeCircle: 60, - ), - ), - ), - title: Text( - otherName, - style: const TextStyle( - color: Colors.black, fontWeight: FontWeight.w600), - ), - subtitle: Text(otherProfession ?? ''), - trailing: const Icon(Icons.keyboard_arrow_right), - ), - ), - ), - Expanded( - child: loading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.only(top: 10), - reverse: true, - child: _messageList(), - ), - ), - Container( - alignment: Alignment.bottomCenter, - width: MediaQuery.of(context).size.width, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), - width: MediaQuery.of(context).size.width, - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: _textController, - style: const TextStyle(color: Colors.black), - decoration: InputDecoration( - hintText: 'Mensaje', - hintStyle: - TextStyle(color: Colors.grey[600], fontSize: 16), - border: OutlineInputBorder( - borderSide: const BorderSide( - color: Colors.grey, width: 1.0), - borderRadius: BorderRadius.circular(50)), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide( - color: Colors.grey, width: 1.0), - borderRadius: BorderRadius.circular(50)), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - filled: true, - fillColor: Colors.grey[200], - ), - onFieldSubmitted: (value) => _sendMessage(), - ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: _sendMessage, - child: Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(30), - ), - child: const Center( - child: Icon(Icons.send, color: Colors.white), - ), - ), - ) - ], - ), - ), - ), - ], - ), - ); - } - - Widget _messageList() { - return Column( - children: [ - ...messages.map( - (e) => uid != e.user - ? ListTile( - title: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(right: 60), - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 16), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: const BorderRadius.only( - topRight: Radius.circular(20), - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - ), - child: Text(e.content, - style: const TextStyle(fontSize: 16)), - ), - const SizedBox(width: 5), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - DateFormat('h:mm a').format(e.timestamp), - style: const TextStyle( - color: Colors.grey, fontSize: 12), - ), - ), - ], - ), - ) - : ListTile( - title: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - margin: const EdgeInsets.only(left: 60), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), - decoration: const BoxDecoration( - color: Color(0xFFD5EFFF), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - ), - child: Text(e.content, - style: const TextStyle(fontSize: 16)), - ), - const SizedBox(width: 5), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - DateFormat('h:mm a').format(e.timestamp), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - ), - ), - ], - ), - ), - ) - ], - ); - } -} diff --git a/lib/src/presentation/screens/cita.dart b/lib/src/presentation/screens/cita.dart deleted file mode 100644 index 0db0239..0000000 --- a/lib/src/presentation/screens/cita.dart +++ /dev/null @@ -1,675 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/presentation/screens/chat.dart'; -import 'package:prosappco/src/presentation/screens/score.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:community_material_icon/community_material_icon.dart'; - -class CitaScreen extends StatefulWidget { - final Event evento; - const CitaScreen({super.key, required this.evento}); - - @override - State createState() => _CitaScreenState(); -} - -class _CitaScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - DateTime today = DateTime.now(); - String nombre = ''; - String numberPhone = ''; - String? photoUrl; - ScoresModel? scoresModel; - bool? ver = true; - bool? pro; - - String formatCurrency(int number) { - final formatter = - NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: ''); - return '\$${formatter.format(number)}'; - } - - Future _openMap(double lat, double lng) async { - final Uri url = - Uri.parse('https://www.google.com/maps/search/?api=1&query=$lat,$lng'); - if (!await launchUrl(url)) { - throw Exception('Could not launch $url'); - } - } - - Future _sendWhatsapp(String phoneNumber) async { - final whatsappUrl = - 'https://wa.me/$phoneNumber?text=${Uri.parse('Hola! me contactaste por Prossapp')}'; - if (!await launch(whatsappUrl)) { - throw Exception('Could not launch $whatsappUrl'); - } - } - - SettingModel? settings; - - @override - void initState() { - super.initState(); - - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() => settings = value), - ); - } - - if (scoresModel == null) { - if (uid != widget.evento.userId) { - ScoresModel.scoreTo(widget.evento.userId, false, false).then( - (ScoresModel s) => setState(() { - scoresModel = s; - pro = true; - }), - ); - } else { - ScoresModel.scoreTo(widget.evento.professionalId, true, false).then( - (ScoresModel s) => setState(() { - scoresModel = s; - pro = false; - }), - ); - } - } - - _loadOtherUser(); - } - - Future _loadOtherUser() async { - final targetId = (uid != widget.evento.userId) - ? widget.evento.userId - : widget.evento.professionalId; - try { - final Map data = - await ApiService.instance.get('/users/$targetId'); - if (mounted) { - setState(() { - nombre = data['name'] ?? ''; - photoUrl = data['picture']; - numberPhone = data['phone'] ?? ''; - }); - } - } catch (e) { - print('Error loading user: $e'); - } - } - - static const _toBackendStatus = { - 'pendiente': 'pending', - 'aprobado': 'accepted', - 'denegado': 'cancelled', - 'iniciado': 'active', - 'terminado': 'completed', - }; - - Future _updateStatus(String status) async { - try { - final backendStatus = _toBackendStatus[status] ?? status; - await ApiService.instance - .patch('/services/${widget.evento.id}/status', {'status': backendStatus}); - } catch (e) { - print('Error updating service status: $e'); - } - } - - @override - Widget build(BuildContext context) { - final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day); - - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Servicio'), - body: Column( - children: [ - Expanded( - child: Column( - children: [ - ListTile( - leading: ReferencePhoto( - ref: photoUrl, - size: 50, - sizeCircle: 50, - sizeIcon: 35, - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - nombre, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(widget.evento.day))} ${DateFormat('h:mm a').format(DateTime.parse(widget.evento.range1Hour1))}', - style: const TextStyle( - color: Colors.grey, - fontSize: 16, - ), - ) - ], - ), - subtitle: Row( - children: [ - RatingBar.builder( - initialRating: scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${scoresModel?.total.toString()}) ${scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - ), - widget.evento.userId == widget.evento.professionalId - ? const SizedBox() - : Container( - margin: const EdgeInsets.only( - left: 40, right: 40, top: 20, bottom: 20), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: Row( - children: [ - const Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - const SizedBox(width: 15), - widget.evento.ubicacion != 'sitio' - ? const Text( - 'Servicio a domicilio.', - style: TextStyle( - color: Colors.black, fontSize: 14), - ) - : const Text( - 'Servicio en su sitio / consultorio', - style: TextStyle( - color: Colors.black, fontSize: 14), - ), - ], - ), - ), - settings?.tarifas == true && widget.evento.tarifa != 0 - ? Column( - children: [ - Text( - formatCurrency(widget.evento.tarifa ?? 0), - style: const TextStyle( - fontWeight: FontWeight.w600, fontSize: 25), - ), - const Text('Tarifa consulta', - style: TextStyle(fontSize: 15)), - ], - ) - : const SizedBox(), - const SizedBox(height: 15), - Text( - textAlign: TextAlign.center, - '"${widget.evento.description?.trim()}"', - style: const TextStyle( - color: Colors.grey, fontStyle: FontStyle.italic), - ), - widget.evento.userId == widget.evento.professionalId - ? const SizedBox() - : widget.evento.status == 'accepted' || - widget.evento.status == 'active' - ? const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text( - 'Medios de comunicación con el usuario.', - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - ) - : const SizedBox(height: 10), - widget.evento.userId == widget.evento.professionalId - ? const SizedBox() - : widget.evento.status == 'completed' - ? pro == true - ? widget.evento.professionalScored == true - ? const SizedBox() - : Column( - children: [ - const SizedBox(height: 120), - ElevatedButton( - onPressed: () { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ScoreScreen( - evento: widget.evento, - pro: pro!, - ); - }, - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: - const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Puntuar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ], - ) - : widget.evento.userScored == true - ? const SizedBox() - : Column( - children: [ - const SizedBox(height: 120), - ElevatedButton( - onPressed: () { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ScoreScreen( - evento: widget.evento, - pro: pro!, - ); - }, - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: - const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Puntuar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ], - ) - : widget.evento.status == 'accepted' || - widget.evento.status == 'active' - ? Row( - children: [ - const Expanded(child: SizedBox()), - ElevatedButton( - onPressed: () => launch("tel:$numberPhone"), - style: ElevatedButton.styleFrom( - foregroundColor: const Color(0xFF2BA4EC), - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - side: const BorderSide( - color: Color(0xFF2BA4EC), - width: 2, - ), - ), - ), - child: const Padding( - padding: EdgeInsets.symmetric( - vertical: 18, horizontal: 0), - child: Icon( - Icons.phone_android, - size: 30, - color: Color(0xFF2BA4EC), - ), - ), - ), - const SizedBox(width: 20), - ElevatedButton( - onPressed: () async { - if (widget.evento.status != 'pending') { - // Start or retrieve chat via API - try { - await ApiService.instance.post( - '/chat/start/${widget.evento.professionalId}', - {}); - } catch (_) {} - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ChatScreen( - eventoId: widget.evento.id); - }, - ), - ); - } else { - showAppSnackBar( - 'El profesional aún no ha aceptado', - 'Debes esperar a que el profesional acepte tu solicitud.', - color: Colors.grey.shade700, - ); - } - }, - style: ElevatedButton.styleFrom( - foregroundColor: const Color(0xFF2BA4EC), - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - side: const BorderSide( - color: Color(0xFF2BA4EC), - width: 2, - ), - ), - ), - child: const Padding( - padding: EdgeInsets.symmetric( - vertical: 18, horizontal: 0), - child: Icon( - Icons.message, - size: 30, - color: Color(0xFF2BA4EC), - ), - ), - ), - const SizedBox(width: 20), - ElevatedButton( - onPressed: () { - _sendWhatsapp(numberPhone); - }, - style: ElevatedButton.styleFrom( - foregroundColor: const Color(0xFF2BA4EC), - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - side: const BorderSide( - color: Color(0xFF2BA4EC), - width: 2, - ), - ), - ), - child: const Padding( - padding: EdgeInsets.symmetric( - vertical: 18, horizontal: 0), - child: Icon( - CommunityMaterialIcons.whatsapp, - size: 30, - color: Color(0xFF2BA4EC), - ), - ), - ), - const Expanded(child: SizedBox()), - ], - ) - : const SizedBox(), - widget.evento.ubicacion == 'sitio' - ? const SizedBox() - : Padding( - padding: const EdgeInsets.only(top: 40), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - _openMap(widget.evento.latitud!, - widget.evento.longitud!); - }, - style: ElevatedButton.styleFrom( - foregroundColor: const Color(0xFF2BA4EC), - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - side: const BorderSide( - color: Color(0xFF2BA4EC), - width: 2, - ), - ), - ), - child: const Padding( - padding: EdgeInsets.symmetric( - vertical: 18, horizontal: 0), - child: Icon( - Icons.near_me, - size: 30, - color: Color(0xFFFFFFFF), - ), - ), - ), - const SizedBox(width: 20), - SizedBox( - width: 200, - child: Text('${widget.evento.address}'), - ) - ], - ), - ), - ], - ), - ), - widget.evento.status == 'accepted' - ? Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: eventDate.year == today.year && - eventDate.month == today.month && - eventDate.day == today.day - ? (DateTime.now() - .difference(DateTime.parse( - widget.evento.range1Hour1)) - .abs() <= - const Duration(minutes: 30) && - ver == true) - ? ElevatedButton( - onPressed: () async { - await _updateStatus('iniciado'); - setState(() => ver = false); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Iniciar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ) - : const SizedBox() - : const SizedBox(), - ), - widget.evento.professionalId == uid - ? ElevatedButton( - onPressed: () async { - await _updateStatus('denegado'); - Navigator.pushReplacementNamed( - context, '/solicitud'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFEC2B2B), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Cancelar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ) - : const SizedBox(), - ], - ), - ) - : const SizedBox(), - widget.evento.status == 'pending' - ? Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: widget.evento.professionalId == uid - ? ElevatedButton( - onPressed: () async { - await _updateStatus('aprobado'); - Navigator.pushReplacementNamed( - context, '/solicitud'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Aceptar', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ) - : const SizedBox(), - ), - ElevatedButton( - onPressed: () async { - await _updateStatus('denegado'); - Navigator.pushReplacementNamed(context, '/solicitud'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFEC2B2B), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Cancelar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ], - ), - ) - : const SizedBox(), - widget.evento.status == 'active' || ver == false - ? Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Column( - children: [ - ElevatedButton( - onPressed: () async { - await _updateStatus('terminado'); - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ScoreScreen( - evento: widget.evento, - pro: pro!, - ); - }, - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Terminar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ], - ), - ) - : const SizedBox(), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/city.dart b/lib/src/presentation/screens/city.dart deleted file mode 100644 index e3e0603..0000000 --- a/lib/src/presentation/screens/city.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:diacritic/diacritic.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class CityScreen extends StatefulWidget { - const CityScreen({super.key}); - - @override - State createState() => _CityScreenState(); -} - -class City { - String? cityName; - String? coordsOfCity; - String? stateOfCity; - String? countryOfCity; - - City({ - this.cityName, - this.coordsOfCity, - this.stateOfCity, - this.countryOfCity, - }); - - @override - String toString() { - return "${cityName ?? ""}, ${coordsOfCity ?? ""}, ${stateOfCity ?? ""}, ${countryOfCity ?? ""}"; - } -} - -Future> getCountries() async { - List citys = []; - - try { - final List countries = - await ApiService.instance.get('/locations/countries'); - - for (final country in countries) { - final String countryName = country['name'] ?? ''; - final List regions = country['regions'] ?? []; - - for (final region in regions) { - final String regionName = region['name'] ?? ''; - final List cities = region['cities'] ?? []; - - for (final city in cities) { - citys.add(City( - cityName: city['name'] ?? '', - coordsOfCity: - '${city['latitude'] ?? 0},${city['longitude'] ?? 0}', - stateOfCity: regionName, - countryOfCity: countryName, - )); - } - } - } - } catch (e) { - print('$e'); - } - - return citys; -} - -class _CityScreenState extends State { - List? filteredCities; - - TextEditingController searchController = TextEditingController(); - - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List? _cities; - - @override - void initState() { - super.initState(); - searchController.addListener(() { - setState(() { - if (_cities != null) { - if (searchController.text.isEmpty) { - filteredCities = _cities!; - } else { - filteredCities = _cities! - .where((city) => removeDiacritics(city.cityName!) - .toLowerCase() - .contains( - removeDiacritics(searchController.text.toLowerCase()))) - .toList(); - } - } - }); - }); - - if (_cities == null) { - getCountries().then((List element) => setState(() { - _cities = element; - filteredCities = element; - })); - } - } - - Future updateCity(String cityName, String coordsCity) async { - try { - await ApiService.instance.patch('/users/me', {'city': cityName}); - } catch (e) { - print('Error al actualizar la ciudad: $e'); - } - } - - @override - Widget build(BuildContext context) { - if (filteredCities == null) { - return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF2BA4EC)), - ), - ); - } - var citys = filteredCities!; - - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Selecciona tu ciudad'), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 10, right: 10, top: 10), - child: TextField( - controller: searchController, - decoration: const InputDecoration( - hintText: 'Busca una ciudad', - prefixIcon: Icon(Icons.near_me), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: citys.length, - itemBuilder: (BuildContext context, int index) { - return ListTile( - title: RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 18.0, - color: Colors.black, - ), - children: [ - TextSpan( - text: '${citys[index].cityName ?? ""}, ', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: - "${citys[index].stateOfCity ?? ""}, ${citys[index].countryOfCity ?? ""}", - style: TextStyle(color: Colors.grey[600]), - ), - ], - ), - ), - onTap: () { - updateCity(citys[index].cityName ?? "", - citys[index].coordsOfCity ?? ""); - Navigator.pop(context, citys[index].cityName ?? ""); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/code_validation.dart b/lib/src/presentation/screens/code_validation.dart deleted file mode 100644 index 66709e6..0000000 --- a/lib/src/presentation/screens/code_validation.dart +++ /dev/null @@ -1,273 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; -import 'package:prosappco/src/components/bottom_sheet.dart'; -import 'package:prosappco/src/components/column_padding.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; -import 'package:prosappco/src/controllers/otp_controller.dart'; -import 'package:prosappco/src/controllers/phone_auth_controller.dart'; -import 'package:responsive_builder/responsive_builder.dart'; - -class CodeValidationScreen extends StatelessWidget { - CodeValidationScreen({super.key, this.phoneNumber}); - String? phoneNumber; - var otp; - - final controller = OTPController.instance; - - @override - Widget build(BuildContext context) { - return ScreenTypeLayout.builder( - mobile: (BuildContext context) => _mobileView(context), - tablet: (BuildContext context) => _mobileView(context), - desktop: (BuildContext context) => _desktopView(context), - ); - } - - Widget _mobileView(BuildContext context) { - return BottomSheetExpanded( - horizontalPadding: 10, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - const Text( - 'Valida el código', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ColumnPadding( - alineacion: MainAxisAlignment.start, - padding: const EdgeInsets.symmetric(horizontal: 25), - children: [ - const SizedBox(height: 10), - const SizedBox( - width: double.infinity, - child: Text( - 'Numero de celular', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B), - ), - ), - ), - const SizedBox(height: 10), - Row( - children: [ - Expanded( - child: TextField( - onChanged: (value) { - phoneNumber = value; - }, - controller: TextEditingController(text: phoneNumber ?? ''), - decoration: const InputDecoration( - border: InputBorder.none, - hintText: '', - suffixIcon: Icon(Icons.edit), - ), - ), - ), - TextButton( - child: const Text('Reenviar código'), - onPressed: () { - if (phoneNumber!.isNotEmpty) { - PhoneAuthController.instance.phoneAuthentication( - phoneNumber!, - ); - } - }, - ), - ], - ), - const SizedBox(height: 10), - const SizedBox( - width: double.infinity, - child: Text( - 'Codigo', - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B), - ), - ), - ), - const SizedBox(height: 10), - OtpTextField( - numberOfFields: 6, - focusedBorderColor: Colors.blue, - fillColor: Colors.black.withOpacity(0.1), - filled: true, - keyboardType: TextInputType.number, - onSubmit: (code) { - otp = code; - OTPController.instance.verifyOTP(otp); - }, - ), - const SizedBox(height: 40), - PrimaryButtom( - onPressed: () { - OTPController.instance.verifyOTP(otp); - }, - label: 'Valida el código', - ), - const SizedBox(height: 30), - ], - ), - ], - ); - } - - Widget _desktopView(BuildContext context) { - double height = MediaQuery.of(context).size.height; - double width = MediaQuery.of(context).size.width; - return Scaffold( - backgroundColor: const Color(0xFFD6F4FF), - body: SizedBox( - height: height, - width: width, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: SizedBox( - height: height, - child: const Center( - child: Image( - image: AssetImage('images/logo_prosapp.png'), - ), - ), - ), - ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: width * 0.07), - color: Colors.white, - height: height, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - SizedBox(width: width * 0.01), - const Text( - 'Validar código', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 38.0, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ColumnPadding( - alineacion: MainAxisAlignment.start, - padding: const EdgeInsets.symmetric(horizontal: 25), - children: [ - const SizedBox(height: 10), - const SizedBox( - width: double.infinity, - child: Text( - 'Numero de celular', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B), - ), - ), - ), - const SizedBox(height: 10), - Row( - children: [ - Expanded( - child: TextField( - onChanged: (value) { - phoneNumber = value; - }, - controller: TextEditingController( - text: phoneNumber ?? ''), - decoration: const InputDecoration( - border: InputBorder.none, - hintText: '', - suffixIcon: Icon(Icons.edit), - ), - ), - ), - TextButton( - child: const Text('Reenviar código'), - onPressed: () { - if (phoneNumber!.isNotEmpty) { - PhoneAuthController.instance - .phoneAuthentication( - phoneNumber!, - ); - } - }, - ), - ], - ), - const SizedBox(height: 10), - const SizedBox( - width: double.infinity, - child: Text( - 'Codigo', - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B), - ), - ), - ), - const SizedBox(height: 10), - OtpTextField( - numberOfFields: 6, - focusedBorderColor: Colors.blue, - fillColor: Colors.black.withOpacity(0.1), - filled: true, - keyboardType: TextInputType.number, - onSubmit: (code) { - otp = code; - OTPController.instance.verifyOTP(otp); - }, - ), - const SizedBox(height: 40), - PrimaryButtom( - onPressed: () { - OTPController.instance.verifyOTP(otp); - }, - label: 'Valida el código', - ), - const SizedBox(height: 30), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/configuracion.dart b/lib/src/presentation/screens/configuracion.dart deleted file mode 100644 index fe64aba..0000000 --- a/lib/src/presentation/screens/configuracion.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/presentation/screens/about.dart'; - -class ConfiguracionScreen extends StatefulWidget { - const ConfiguracionScreen({super.key}); - - @override - State createState() => _ConfiguracionScreenState(); -} - -class _ConfiguracionScreenState extends State { - Future _showDeleteAccountConfirmationDialog( - BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Eliminar Cuenta'), - content: const Text( - '¿Estás seguro de que deseas eliminar tu cuenta? Esta acción no se puede deshacer.'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancelar'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - showAppSnackBar('Eliminar cuenta', 'Para eliminar tu cuenta contacta a soporte.', color: Colors.grey.shade700); - }, - child: const Text( - 'Eliminar', - style: - TextStyle(color: Colors.red, fontWeight: FontWeight.w600), - ), - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Configuración'), - body: ListView( - children: [ - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const AboutScreen(); - }, - ), - ); - }, - title: const Text('Acerca de la aplicación'), - trailing: const Icon( - Icons.keyboard_arrow_right, - color: Colors.black, - ), - ), - ListTile( - onTap: () { - _showDeleteAccountConfirmationDialog(context); - }, - title: const Text( - 'Eliminar cuenta', - style: TextStyle(color: Colors.red), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/horario.dart b/lib/src/presentation/screens/horario.dart deleted file mode 100644 index 39d3582..0000000 --- a/lib/src/presentation/screens/horario.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import '../../components/schedule_picker.dart'; - -class HorarioScreen extends StatelessWidget { - Map horarios; - HorarioScreen({super.key, required this.horarios}); - - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - Future updateHorario(BuildContext context) async { - try { - Map horariosMap = {}; - - horarios.forEach((key, value) { - if (value.habilitado && !value.jornadaContinua) { - if (value.range1Hour1 == null || - value.range1Hour2 == null || - value.range2Hour1 == null || - value.range2Hour2 == null) { - value.habilitado = false; - value.jornadaContinua = false; - } - } - - if (value.habilitado && value.jornadaContinua) { - if (value.range1Hour1 == null || value.range2Hour2 == null) { - value.habilitado = false; - value.jornadaContinua = false; - } - } - - horariosMap[key] = { - 'habilitado': value.habilitado, - 'jornadaContinua': value.jornadaContinua, - 'range1Hour1': formatTimeOfDay(value.range1Hour1), - 'range1Hour2': formatTimeOfDay(value.range1Hour2), - 'range2Hour1': formatTimeOfDay(value.range2Hour1), - 'range2Hour2': formatTimeOfDay(value.range2Hour2), - }; - }); - - // Convert map keyed by day number to array format expected by backend - final List> schedulesList = []; - horariosMap.forEach((key, value) { - schedulesList.add({ - 'day_of_week': int.tryParse(key) ?? 1, - 'enabled': value['habilitado'] ?? false, - 'continuous_day': value['jornadaContinua'] ?? false, - 'range1_hour1': _fmtTime(value['range1Hour1']), - 'range1_hour2': _fmtTime(value['range1Hour2']), - 'range2_hour1': _fmtTime(value['range2Hour1']), - 'range2_hour2': _fmtTime(value['range2Hour2']), - }); - }); - await ApiService.instance - .patch('/professionals/me/schedules', {'schedules': schedulesList}); - } catch (e) { - print('Error al actualizar el horario: $e'); - } - } - - // Convert "8:30 AM" / "HH:MM" strings to "HH:MM" for the backend - String? _fmtTime(String? t) { - if (t == null) return null; - try { - final clean = t.replaceAll(RegExp(r'\s?[APM]+', caseSensitive: false), '').trim(); - final parts = clean.split(':'); - return '${int.parse(parts[0]).toString().padLeft(2, '0')}:${(parts.length > 1 ? int.parse(parts[1]) : 0).toString().padLeft(2, '0')}'; - } catch (_) { - return null; - } - } - - String? formatTimeOfDay(TimeOfDay? time) { - if (time != null) { - final now = DateTime.now(); - final dateTime = - DateTime(now.year, now.month, now.day, time.hour, time.minute); - final format = DateFormat.jm(); - return format.format(dateTime); - } - return null; - } - - int _dayOfWeekToInt(String dayOfWeek) { - switch (dayOfWeek) { - case '1': return 1; - case '2': return 2; - case '3': return 3; - case '4': return 4; - case '5': return 5; - case '6': return 6; - case '7': return 7; - default: throw ArgumentError('Invalid day of week: $dayOfWeek'); - } - } - - String _stringToDayOfWeek(String dayOfWeek) { - switch (dayOfWeek) { - case '1': return 'Lunes'; - case '2': return 'Martes'; - case '3': return 'Miércoles'; - case '4': return 'Jueves'; - case '5': return 'Viernes'; - case '6': return 'Sábado'; - case '7': return 'Domingo'; - default: throw ArgumentError('Invalid day of week: $dayOfWeek'); - } - } - - @override - Widget build(BuildContext context) { - final sortedHorarios = Map.fromEntries( - horarios.entries.toList() - ..sort( - (a, b) => _dayOfWeekToInt(a.key).compareTo(_dayOfWeekToInt(b.key))), - ); - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Horario'), - body: SingleChildScrollView( - child: Column( - children: [ - const Divider(height: 5), - Column( - children: sortedHorarios.entries.map( - (entry) { - return SchedulePicker( - name: _stringToDayOfWeek(entry.key), - schedule: entry.value, - ); - }, - ).toList(), - ), - Padding( - padding: const EdgeInsets.only(top: 30, bottom: 30), - child: PrimaryButtom( - onPressed: () async { - await updateHorario(context); - Navigator.pop(context); - }, - label: 'Guardar', - ), - ) - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/login/login.dart b/lib/src/presentation/screens/login/login.dart deleted file mode 100644 index eeff8b7..0000000 --- a/lib/src/presentation/screens/login/login.dart +++ /dev/null @@ -1,456 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:intl_phone_field/intl_phone_field.dart'; -import 'package:prosappco/src/components/bottom_sheet.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; -import 'package:prosappco/src/controllers/phone_auth_controller.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:prosappco/src/presentation/screens/code_validation.dart'; -import 'package:prosappco/src/presentation/screens/web_view.dart'; -import 'package:provider/provider.dart'; -import 'package:responsive_builder/responsive_builder.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - final controller = PhoneAuthController.instance; - final _formKey = GlobalKey(); - String completePhoneNumber = ''; - bool _isChecked = false; - SettingModel? settings; - - void _clearPhoneNumber() { - if (mounted) { - setState(() { - controller.phoneNo.text = ''; - }); - } - } - - void _launchURL(String url) async { - if (await canLaunch(url)) { - await launch(url, forceSafariVC: false, forceWebView: false); - } else { - throw 'No se pudo abrir el enlace $url'; - } - } - - @override - void initState() { - super.initState(); - if (settings == null) { - SettingModel.getSettings().then((SettingModel value) { - if (mounted) { - setState(() { - settings = value; - }); - } - }); - } - } - - @override - Widget build(BuildContext context) { - return ScreenTypeLayout.builder( - mobile: (BuildContext context) => _mobileView(context), - tablet: (BuildContext context) => _mobileView(context), - desktop: (BuildContext context) => _desktopView(context), - ); - } - - Widget _mobileView(BuildContext context) { - return BottomSheetExpanded( - children: [ - const SizedBox( - width: double.infinity, - child: Text( - 'Iniciar sesión', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 10), - const SizedBox( - width: double.infinity, - child: Text( - 'Numero de celular', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B), - ), - ), - ), - Form( - key: _formKey, - child: IntlPhoneField( - controller: controller.phoneNo, - initialCountryCode: 'CO', - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (phoneNo) { - completePhoneNumber = phoneNo.completeNumber; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - ), - ), - ), - const Text( - 'Un código será enviado a este numero de celular.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13.0, - color: Color(0xFF65676B), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Checkbox( - value: _isChecked, - onChanged: (value) { - setState(() { - _isChecked = value!; - }); - }, - ), - GestureDetector( - onTap: () { - if (kIsWeb) { - _launchURL(settings?.terminosCondiciones ?? ''); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Términos y condiciones', - link: settings?.terminosCondiciones ?? '', - ); - }, - ), - ); - } - }, - child: const Text( - 'Acepto los términos y condiciones.', - style: TextStyle( - fontSize: 13.0, - color: Color(0xFF65676B), - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), - ), - PrimaryButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - PhoneAuthController.instance.phoneAuthentication( - completePhoneNumber.trim(), - ); - _clearPhoneNumber(); - await Navigator.push( - context, - MaterialPageRoute( - builder: (_) => CodeValidationScreen( - phoneNumber: completePhoneNumber.trim(), - ), - ), - ); - Provider.of(context, listen: false) - .initUserProvider(); - } - }, - text: 'Enviar código', - isEnabled: _isChecked, - ), - const SizedBox(height: 20), - GestureBottom(clearPhoneNumber: _clearPhoneNumber), - const SizedBox(height: 20), - const RichTxTBottom(), - const SizedBox(height: 20), - ], - ); - } - - Widget _desktopView(BuildContext context) { - double height = MediaQuery.of(context).size.height; - double width = MediaQuery.of(context).size.width; - return Scaffold( - backgroundColor: const Color(0xFFD6F4FF), - body: SizedBox( - height: height, - width: width, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: SizedBox( - height: height, - child: const Center( - child: Image( - image: AssetImage('images/logo_prosapp.png'), - ), - ), - ), - ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: width * 0.1), - color: Colors.white, - height: height, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - width: double.infinity, - child: Text( - 'Iniciar sesión', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 20), - const SizedBox( - width: double.infinity, - child: Text( - 'Numero de celular', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B), - ), - ), - ), - Form( - key: _formKey, - child: IntlPhoneField( - controller: controller.phoneNo, - initialCountryCode: 'CO', - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ], - onChanged: (phoneNo) { - completePhoneNumber = phoneNo.completeNumber; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - ), - ), - ), - const Text( - 'Se enviará un código a este número de celular.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13.0, - color: Color(0xFF65676B), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Checkbox( - value: _isChecked, - onChanged: (value) { - setState(() { - _isChecked = value!; - }); - }, - ), - GestureDetector( - onTap: () { - if (kIsWeb) { - _launchURL(settings?.terminosCondiciones ?? ''); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Términos y condiciones', - link: - settings?.terminosCondiciones ?? '', - ); - }, - ), - ); - } - }, - child: const Text( - 'Acepto los términos y condiciones.', - style: TextStyle( - fontSize: 13.0, - color: Color(0xFF65676B), - decoration: TextDecoration - .underline, // Add underline style - ), - ), - ), - ], - ), - ), - PrimaryButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - PhoneAuthController.instance.phoneAuthentication( - completePhoneNumber.trim(), - ); - await Navigator.push( - context, - MaterialPageRoute( - builder: (_) => CodeValidationScreen( - phoneNumber: completePhoneNumber.trim(), - ), - ), - ); - _clearPhoneNumber(); - Provider.of(context, listen: false) - .initUserProvider(); - } - }, - text: 'Enviar código', - isEnabled: _isChecked, - ), - const SizedBox(height: 20), - GestureBottom(clearPhoneNumber: _clearPhoneNumber), - const SizedBox(height: 20), - const RichTxTBottom(), - ], - ), - ), - ), - ], - ), - ), - ); - } -} - -class GestureBottom extends StatelessWidget { - final VoidCallback clearPhoneNumber; - - GestureBottom({ - super.key, - required this.clearPhoneNumber, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - clearPhoneNumber(); - Navigator.pushNamed(context, '/login'); - }, - child: const Text( - 'Inicia sesión con tu correo electrónico', - style: TextStyle( - fontSize: 15.0, color: Color(0xFF65676B), - decoration: TextDecoration.underline, // Subrayado - ), - ), - ); - } -} - -class RichTxTBottom extends StatelessWidget { - const RichTxTBottom({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 16.0, - color: Color(0xFF65676B), - fontFamily: 'Poppins', - ), - children: [ - const TextSpan(text: '¿No estás registrado? '), - WidgetSpan( - child: GestureDetector( - onTap: () { - Navigator.pushNamed(context, '/register'); - }, - child: const Text( - 'Regístrate', - style: TextStyle( - fontSize: 16.0, - color: Color(0xFF2BA4EC), - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/login/login_email.dart b/lib/src/presentation/screens/login/login_email.dart deleted file mode 100644 index 0f6d9f8..0000000 --- a/lib/src/presentation/screens/login/login_email.dart +++ /dev/null @@ -1,550 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/bottom_sheet.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; -import 'package:prosappco/src/controllers/login_email_controller.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:provider/provider.dart'; -import 'package:responsive_builder/responsive_builder.dart'; - -class LoginEmailScreen extends StatefulWidget { - const LoginEmailScreen({super.key}); - - @override - State createState() => _LoginEmailScreenState(); -} - -class _LoginEmailScreenState extends State { - bool _obscureText = true; - final controller = LoginEmailController.instance; - final _formKey = GlobalKey(); - SettingModel? settings; - - @override - void initState() { - super.initState(); - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - }), - ); - } - } - - @override - Widget build(BuildContext context) { - return ScreenTypeLayout.builder( - mobile: (BuildContext context) => _mobileView(context), - tablet: (BuildContext context) => _mobileView(context), - desktop: (BuildContext context) => _desktopView(context), - ); - } - - Widget _mobileView(BuildContext context) { - bool isIOS = Theme.of(context).platform == TargetPlatform.iOS; - - return BottomSheetExpanded( - horizontalPadding: 10, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - const Text( - 'Iniciar sesión', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.right, - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 25), - child: Form( - key: _formKey, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20), - child: Column( - children: [ - !isIOS && !kIsWeb && settings?.google == true - ? Padding( - padding: const EdgeInsets.only(bottom: 20), - child: ElevatedButton( - onPressed: () async { - await AuthenticationRepository.instance - .signInWithGoogle() - .then((value) => { - Provider.of(context, - listen: false) - .initUserProvider() - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Entra con Google ', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - SizedBox(width: 5), - const Text('G', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)), - ], - )), - ) - : const SizedBox(), - !isIOS && !kIsWeb && settings?.google == true - ? const Padding( - padding: EdgeInsets.symmetric(vertical: 5), - child: Row( - children: [ - Expanded( - child: Divider( - color: Colors.black38, - thickness: 1, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Text("ó"), - ), - Expanded( - child: Divider( - color: Colors.black38, - thickness: 1, - ), - ), - ], - ), - ) - : const SizedBox(), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Email', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - ), - ), - FormEmail(controller: controller), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Password', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - ), - ), - TextFormField( - controller: controller.password, - obscureText: _obscureText, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingresa una contraseña'; - } - return null; - }, - decoration: InputDecoration( - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - border: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - errorBorder: const OutlineInputBorder( - borderSide: - BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - hintText: 'Contraseña', - fillColor: const Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - hintStyle: const TextStyle( - color: Colors.grey, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: TextButton( - onPressed: () { - Navigator.pushNamed(context, '/resetpassword'); - }, - child: const Text( - 'Olvidé la contraseña', - style: TextStyle( - color: Colors.blue, - ), - ), - ), - ), - PaddingButtomBottom( - formKey: _formKey, - controller: controller, - ), - const RichTxtBottom() - ], - ), - ), - ), - ), - ], - ); - } - - Widget _desktopView(BuildContext context) { - double height = MediaQuery.of(context).size.height; - double width = MediaQuery.of(context).size.width; - return Scaffold( - backgroundColor: const Color(0xFFD6F4FF), - body: SizedBox( - height: height, - width: width, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: SizedBox( - height: height, - child: const Center( - child: Image( - image: AssetImage('images/logo_prosapp.png'), - ), - ), - ), - ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: width * 0.07), - color: Colors.white, - height: height, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - SizedBox(width: width * 0.01), - const Text( - 'Iniciar sesión', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.right, - ), - ], - ), - Form( - key: _formKey, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 0, vertical: 20), - child: Column(children: [ - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Email', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B))), - )), - FormEmail(controller: controller), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Password', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B))), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 50), - child: TextFormField( - controller: controller.password, - obscureText: _obscureText, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingresa una contraseña'; - } - return null; - }, - decoration: InputDecoration( - enabledBorder: const OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: const OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - border: const OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - hintText: 'Contraseña', - hintStyle: const TextStyle( - color: Colors.grey, - ), - fillColor: - const Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: TextButton( - onPressed: () { - Navigator.pushNamed(context, '/resetpassword'); - }, - child: const Text( - 'Olvidé la contraseña', - style: TextStyle( - color: Colors.blue, - ), - ), - ), - ), - PaddingButtomBottom( - formKey: _formKey, controller: controller), - const RichTxtBottom() - ]), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} - -class FormEmail extends StatelessWidget { - const FormEmail({ - super.key, - required this.controller, - }); - - final LoginEmailController controller; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: TextFormField( - controller: controller.email, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingresa un Email'; - } - final RegExp emailRegExp = - RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegExp.hasMatch(value)) { - return 'Por favor, ingresa un Email válido'; - } - return null; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - hintText: 'Hello@gmail.com', - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: Icon(Icons.email_outlined), - hintStyle: TextStyle( - color: Colors.grey, - ), - ), - ), - ); - } -} - -class PaddingButtomBottom extends StatelessWidget { - const PaddingButtomBottom({ - super.key, - required GlobalKey formKey, - required this.controller, - }) : _formKey = formKey; - - final GlobalKey _formKey; - final LoginEmailController controller; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 25), - child: Center( - child: PrimaryButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - LoginEmailController.instance - .loginUser( - controller.email.text.trim(), - controller.password.text.trim(), - ) - .then((value) { - Provider.of(context, listen: false) - .initUserProvider(); - }); - } - }, - text: 'Iniciar', - ), - ), - ); - } -} - -class RichTxtBottom extends StatelessWidget { - const RichTxtBottom({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 16.0, - color: Color(0xFF65676B), - fontFamily: 'Poppins', - ), - children: [ - const TextSpan(text: '¿No estás registrado? '), - WidgetSpan( - child: GestureDetector( - onTap: () { - Navigator.pushReplacementNamed(context, '/register'); - }, - child: const Text( - 'Registrarse', - style: TextStyle( - fontSize: 16.0, - color: Color(0xFF2BA4EC), - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/map/service.dart b/lib/src/presentation/screens/map/service.dart deleted file mode 100644 index 257ee34..0000000 --- a/lib/src/presentation/screens/map/service.dart +++ /dev/null @@ -1,1250 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:geocoding/geocoding.dart' as geocoding; -import 'package:http/http.dart' as http; -import 'package:geolocator/geolocator.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:flutter_polyline_points/flutter_polyline_points.dart'; -import 'package:intl/intl.dart'; -import 'package:location/location.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:prosappco/constansts.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/network_utility.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/presentation/screens/calendar_pro.dart'; -import 'package:prosappco/src/presentation/screens/professional.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile.dart'; -import 'package:prosappco/src/presentation/screens/service_after.dart'; -import 'package:prosappco/src/presentation/screens/service_type.dart'; -import 'package:prosappco/src/presentation/widgets/shared/drawer_menu.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; -import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ServiceScreen extends StatefulWidget { - const ServiceScreen({ - super.key, - }); - - @override - State createState() => _ServiceScreenState(); -} - -class _ServiceScreenState extends State { - late String appVersion; - final Uri _url = Uri.parse( - 'https://play.google.com/store/apps/details?id=com.prosapp.prosapp'); - final Uri _urlIos = - Uri.parse('https://apps.apple.com/co/app/prosapp/id6469028900'); - - final Location _locationController = Location(); - - final Completer _mapController = - Completer(); - - static const LatLng _pGooglePlex = LatLng(7.080486, -73.087447); - LatLng? _currentP; - - Map polylines = {}; - - final TextEditingController _locacionController = TextEditingController(); - final TextEditingController _ubicationController = TextEditingController(); - final TextEditingController _profesionalController = TextEditingController(); - final TextEditingController _serviceTypeController = TextEditingController(); - final TextEditingController _observacionController = TextEditingController(); - final DateTime now = DateTime.now(); - EventoService eventoService = EventoService(); - String _serviceType = 'Servicio'; - String ubicacion = ''; - String professionalId = ''; - String professionalToken = ''; - String professionalAddress = ''; - String professionalUbicacion = ''; - int? professionalTarifa; - double? professionalLatitude; - double? professionalLongitude; - - String _ciudad = '...'; - - final DateFormat formatter = DateFormat('dd/MM/yyyy'); - dynamic coordinates; - Set markers = {}; - BitmapDescriptor? _markerIcon; - List _placesList = []; - String _coordsOfCity = '0.0,0.0'; - - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - String? settings; - - final _nameController = TextEditingController(); - - DateTime? fechaSeleccionada; - TimeOfDay? horaSeleccionada; - Professional? profesionalSeleccionado; - - @override - void initState() { - super.initState(); - - _getAppVersion(); - - getLocationUpdates(); - - // Load user name from in-memory currentUser - final authUser = AuthenticationRepository.instance.currentUser; - if (authUser != null && authUser.name.isNotEmpty) { - _nameController.text = authUser.name; - } - - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => (value) { - if (mounted) { - setState(() { - settings = value.version; - _getAppVersion(); - }); - } - }, - ); - } - - // Load city from in-memory user - if (_ciudad == '...') { - final city = authUser?.city ?? ''; - if (city.isNotEmpty) { - _ciudad = city; - } else { - _ciudad = 'Cúcuta'; - } - } - - if (Platform.isAndroid) { - BitmapDescriptor.fromAssetImage( - const ImageConfiguration(size: Size(2, 2)), - 'images/pro_marke_android.png', - ).then((icon) { - if (mounted) { - setState(() { - _markerIcon = icon; - }); - } - }); - } else { - BitmapDescriptor.fromAssetImage( - const ImageConfiguration(size: Size(1, 1)), - 'images/pro_marke.png', - ).then((icon) { - if (mounted) { - setState(() { - _markerIcon = icon; - }); - } - }); - } - - updateMarkersForServiceType(_serviceType); - } - - @override - void dispose() { - _locacionController.dispose(); - _ubicationController.dispose(); - _profesionalController.dispose(); - _serviceTypeController.dispose(); - _observacionController.dispose(); - - _locationController.onLocationChanged.listen((_) {}).cancel(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final userProvider = Provider.of(context); - UserModel? user = userProvider.user; - dynamic datos; - - return SafeArea( - child: Scaffold( - drawer: DrawerMenu(), - body: Column( - children: [ - Expanded( - child: Stack( - children: [ - GoogleMap( - onMapCreated: ((GoogleMapController controller) { - _mapController.complete(controller); - - _applyMapStyle(controller); - }), - initialCameraPosition: const CameraPosition( - target: _pGooglePlex, - zoom: 15, - ), - markers: { - ...markers, - if (_currentP != null) - Marker( - markerId: const MarkerId("_currentLocation"), - icon: BitmapDescriptor.defaultMarker, - position: _currentP!, - ), - }, - polylines: Set.of(polylines.values), - myLocationButtonEnabled: false, - onCameraIdle: () { - if (professionalAddress == '') { - if (coordinates != null) { - getLocationName( - coordinates.latitude, coordinates.longitude) - .then((locationName) { - if (mounted) { - setState(() { - _locacionController.text = locationName; - }); - } - }); - } - } - }, - onCameraMove: (position) { - if (mounted) { - setState(() { - coordinates = position.target; - }); - } - }, - ), - Positioned( - top: 10, - left: 10, - child: Builder( - builder: (context) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - shape: const CircleBorder(), - elevation: 3, - minimumSize: const Size(50, 50), - ), - child: const Icon( - Icons.menu, - color: Colors.black, - size: 35, - ), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ); - }, - ), - ), - const Positioned( - bottom: 25, - right: 0, - left: 0, - top: 0, - child: Icon( - Icons.location_on, - size: 40, - color: Color(0xFFFF0000), - ), - ), - Positioned( - top: 10, - right: 20, - child: FloatingActionButton( - onPressed: () async { - try { - Position position = await _determinePosition(); - - if (mounted) { - setState(() { - _currentP = LatLng( - position.latitude, - position.longitude, - ); - }); - - _animateCameraToPosition(_currentP!); - } - } catch (e) { - WarningSnackbar.show( - title: 'Ubicación desactivada', - message: - 'Por favor activa la ubicacion de tu telefono.', - ); - } - }, - elevation: 0, - child: const Icon( - Icons.gps_fixed, - size: 30, - ), - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), - color: Colors.white, - child: Form( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - controller: _serviceTypeController, - readOnly: true, - onTap: () async { - if (_nameController.text == '') { - WarningSnackbar.show( - title: 'Completa tu perfil', - message: - 'Asegurate de llenar todos los campos de tu perfil antes de seleccionar un tipo de servicio.', - ); - - return; - } - - final String? serviceType = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ServiceTypeScreen(); - }, - ), - ) as String?; - - if (serviceType != null) { - if (mounted) { - setState(() { - _serviceType = serviceType; - _serviceTypeController.text = _serviceType; - - updateMarkersForServiceType(_serviceType); - }); - - datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ProfessionalScreen( - profession: _serviceTypeController.text, - ); - }, - ), - ); - - if (datos != null) { - fechaSeleccionada = datos[0]; - horaSeleccionada = datos[1]; - Professional? profesional = datos[2]; - profesionalSeleccionado = profesional; - - if (profesional != null) { - ubicacion = datos[3]; - if (mounted) { - setState(() { - _profesionalController.text = - profesional.name.toString(); - - professionalId = profesional.id; - - if (profesional.token != '') { - professionalToken = profesional.token!; - } - - professionalTarifa = - profesional.tarifa ?? 0; - - professionalUbicacion = - profesional.ubicacion; - - professionalLatitude = profesional.latitude; - professionalLongitude = - profesional.longitude; - - if (ubicacion == 'sitio') { - professionalAddress = - profesional.realAddress; - - _locacionController.text = - profesional.realAddress; - - LatLng professionalCoordinates = LatLng( - professionalLatitude!, - professionalLongitude!); - - if (professionalLatitude != null && - professionalLatitude != 0 && - professionalLongitude != null && - professionalLongitude != 0) { - _animateCameraToPosition( - LatLng(professionalLatitude!, - professionalLongitude!), - ); - - getPolylinePoints(_currentP!, - professionalCoordinates) - .then( - (coordinates) => { - generatePolyLineFromPoints( - coordinates), - }, - ); - } - } else { - polylines.clear(); - } - }); - } - } - } - } - } - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.room_service_rounded), - suffixIcon: Icon(Icons.arrow_drop_down), - hintText: 'Tipo de Servicio', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: _profesionalController, - readOnly: true, - onTap: () async { - if (_currentP == null) { - try { - Position position = await _determinePosition(); - - if (mounted) { - setState(() { - _currentP = LatLng( - position.latitude, - position.longitude, - ); - }); - - _animateCameraToPosition(_currentP!); - } - } catch (e) { - WarningSnackbar.show( - title: 'Ubicación desactivada', - message: - 'Por favor activa la ubicacion de tu telefono para solicitar un servicio.', - ); - } - - return; - } - - if (_nameController.text == '') { - WarningSnackbar.show( - title: 'Completa tu perfil', - message: - 'Asegurate de llenar todos los campos de tu perfil antes de seleccionar un profesional.', - ); - - return; - } - - datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ProfessionalScreen( - profession: _serviceTypeController.text, - ); - }, - ), - ); - - if (datos != null) { - fechaSeleccionada = datos[0]; - horaSeleccionada = datos[1]; - Professional? profesional = datos[2]; - profesionalSeleccionado = profesional; - - if (profesional != null) { - ubicacion = datos[3]; - if (mounted) { - setState(() { - _profesionalController.text = - profesional.name.toString(); - - professionalId = profesional.id; - - if (profesional.token != '') { - professionalToken = profesional.token!; - } - - professionalTarifa = profesional.tarifa ?? 0; - - professionalUbicacion = profesional.ubicacion; - - professionalLatitude = profesional.latitude; - professionalLongitude = profesional.longitude; - - if (ubicacion == 'sitio') { - professionalAddress = profesional.realAddress; - - _locacionController.text = - profesional.realAddress; - - LatLng professionalCoordinates = LatLng( - professionalLatitude!, - professionalLongitude!); - - if (professionalLatitude != null && - professionalLatitude != 0 && - professionalLongitude != null && - professionalLongitude != 0) { - _animateCameraToPosition( - LatLng(professionalLatitude!, - professionalLongitude!), - ); - - getPolylinePoints( - _currentP!, professionalCoordinates) - .then( - (coordinates) => { - generatePolyLineFromPoints(coordinates), - }, - ); - } - } else { - polylines.clear(); - } - }); - } - } - } - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.assignment_ind_rounded), - suffixIcon: Icon(Icons.arrow_drop_down), - hintText: 'Seleccionar profesional', - ), - ), - const SizedBox(height: 15), - TextFormField( - controller: _locacionController, - readOnly: ubicacion == 'sitio' ? true : false, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.near_me), - hintText: 'Dirección', - ), - onChanged: (value) { - String modifiedValue = value.replaceAll(' ', '_'); - placeAutoComplete(modifiedValue); - }, - ), - ubicacion == 'sitio' - ? const SizedBox() - : SizedBox( - height: _placesList.isNotEmpty ? 200 : 0, - child: ListView.builder( - itemCount: _placesList.length, - itemBuilder: (context, index) { - return ListTile( - onTap: () { - final selectedAddress = - _placesList[index]['formatted_address']; - final selectedLatitude = _placesList[index] - ['geometry']['location']['lat']; - final selectedLongitude = _placesList[index] - ['geometry']['location']['lng']; - - _locacionController.text = selectedAddress; - - _animateCameraToPosition(LatLng( - selectedLatitude, - selectedLongitude, - )); - - if (mounted) { - setState(() { - _placesList = []; - }); - } - }, - title: Text( - _placesList[index]['formatted_address']), - ); - }, - ), - ), - fechaSeleccionada == null && horaSeleccionada == null - ? const SizedBox() - : const SizedBox(height: 15), - fechaSeleccionada == null && horaSeleccionada == null - ? const SizedBox() - : Row( - children: [ - Expanded( - child: TextFormField( - readOnly: true, - onTap: () async { - datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CalendarProScreen( - professional: - profesionalSeleccionado!, - ); - }, - ), - ); - - if (datos != null) { - fechaSeleccionada = datos[0]; - horaSeleccionada = datos[1]; - Professional? profesional = datos[2]; - profesionalSeleccionado = profesional; - - if (profesional != null) { - if (mounted) { - setState(() { - _profesionalController.text = - profesional.name.toString(); - - professionalId = profesional.id; - - if (profesional.token != '') { - professionalToken = - profesional.token!; - } - - professionalTarifa = - profesional.tarifa ?? 0; - - professionalUbicacion = - profesional.ubicacion; - - professionalLatitude = - profesional.latitude; - professionalLongitude = - profesional.longitude; - - if (ubicacion == 'sitio') { - professionalAddress = - profesional.realAddress; - - _locacionController.text = - profesional.realAddress; - - LatLng professionalCoordinates = - LatLng(professionalLatitude!, - professionalLongitude!); - - if (professionalLatitude != - null && - professionalLatitude != 0 && - professionalLongitude != - null && - professionalLongitude != 0) { - _animateCameraToPosition( - LatLng(professionalLatitude!, - professionalLongitude!), - ); - - getPolylinePoints(_currentP!, - professionalCoordinates) - .then( - (coordinates) => { - generatePolyLineFromPoints( - coordinates), - }, - ); - } - } else { - polylines.clear(); - } - }); - } - } - } - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.calendar_month), - hintText: 'Fecha', - ), - controller: TextEditingController( - text: fechaSeleccionada == null - ? '' - : formatter.format(fechaSeleccionada!), - ), - ), - ), - Expanded( - child: TextFormField( - readOnly: true, - onTap: () async { - datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CalendarProScreen( - professional: - profesionalSeleccionado!, - ); - }, - ), - ); - - if (datos != null) { - fechaSeleccionada = datos[0]; - horaSeleccionada = datos[1]; - Professional? profesional = datos[2]; - profesionalSeleccionado = profesional; - - if (profesional != null) { - if (mounted) { - setState(() { - _profesionalController.text = - profesional.name.toString(); - - professionalId = profesional.id; - - if (profesional.token != '') { - professionalToken = - profesional.token!; - } - - professionalTarifa = - profesional.tarifa ?? 0; - - professionalUbicacion = - profesional.ubicacion; - - professionalLatitude = - profesional.latitude; - professionalLongitude = - profesional.longitude; - - if (ubicacion == 'sitio') { - professionalAddress = - profesional.realAddress; - - _locacionController.text = - profesional.realAddress; - - LatLng professionalCoordinates = - LatLng(professionalLatitude!, - professionalLongitude!); - - if (professionalLatitude != null && - professionalLatitude != 0 && - professionalLongitude != null && - professionalLongitude != 0) { - _animateCameraToPosition( - LatLng(professionalLatitude!, - professionalLongitude!), - ); - - getPolylinePoints(_currentP!, - professionalCoordinates) - .then( - (coordinates) => { - generatePolyLineFromPoints( - coordinates), - }, - ); - } - } else { - polylines.clear(); - } - }); - } - } - } - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.watch_later_outlined), - hintText: 'Hora', - ), - controller: TextEditingController( - text: horaSeleccionada == null - ? '' - : ' ${horaSeleccionada!.format(context)}', - ), - )) - ], - ), - fechaSeleccionada == null && horaSeleccionada == null - ? const SizedBox() - : const SizedBox(height: 15), - fechaSeleccionada == null && horaSeleccionada == null - ? const SizedBox() - : TextFormField( - controller: _observacionController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.message_outlined), - hintText: 'Observaciones', - ), - ), - const SizedBox(height: 20), - Center( - child: PrimaryButton( - onPressed: () { - if (user?.name == null || - user?.name == '' || - user?.phoneNumber == null || - user?.phoneNumber == '') { - WarningSnackbar.show( - title: 'Completa tu perfil', - message: - 'Diligencia la información de tu perfil antes de solicitar un servicio.', - ); - - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfileScreen(); - }, - ), - ); - } else { - try { - DateTime time1 = DateTime( - fechaSeleccionada!.year, - fechaSeleccionada!.month, - fechaSeleccionada!.day, - horaSeleccionada!.hour, - horaSeleccionada!.minute, - ); - - DateTime time2 = - time1.add(const Duration(hours: 2)); - - UserModel.getUser(uid.toString()).then((value) { - eventoService - .createEvent( - value?.name ?? '', - _observacionController.text, - fechaSeleccionada.toString().substring(0, - fechaSeleccionada.toString().length - 1), - '$time1', - '$time2', - professionalId, - ubicacion, - _locacionController.text, - ubicacion != 'sitio' - ? coordinates.latitude - : professionalLatitude, - ubicacion != 'sitio' - ? coordinates.longitude - : professionalLongitude, - 'pendiente', - professionalTarifa == 0 - ? 0 - : professionalTarifa, - false, - false, - ) - .then((value) { - if (professionalToken != '') { - sendPushNotification(professionalToken); - } - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ServiceAfterScreen( - eventoId: value, - ); - }, - ), - ); - }); - }); - } catch (e) { - WarningSnackbar.show( - title: 'Llena todos los campos', - message: - 'Asegurate de llenar todos los campos antes de solicitar un servicio.', - ); - } - } - }, - text: 'Solicitar servicio', - minWidth: 230, - ), - ), - ], - ), - ), - ) - ], - ), - ), - ); - } - - Future getLocationUpdates() async { - bool _serviceEnabled; - PermissionStatus _permissionGranted; - - _serviceEnabled = await _locationController.serviceEnabled(); - if (_serviceEnabled) { - _serviceEnabled = await _locationController.requestService(); - } else { - return; - } - - _permissionGranted = await _locationController.hasPermission(); - if (_permissionGranted == PermissionStatus.denied) { - _permissionGranted = await _locationController.requestPermission(); - if (_permissionGranted != PermissionStatus.granted) { - return; - } - } - - _locationController.onLocationChanged - .listen((LocationData currentLocation) { - if (currentLocation.latitude != null && - currentLocation.longitude != null) { - if (mounted) { - setState(() { - _currentP = LatLng( - currentLocation.latitude!, - currentLocation.longitude!, - ); - }); - } - } - }); - } - - Future> getPolylinePoints( - LatLng originP, LatLng destinationP) async { - List polylineCoordinates = []; - PolylinePoints polylinePoints = PolylinePoints(); - PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( - GOOGLE_MAPS_API_KEY, - PointLatLng(originP.latitude, originP.longitude), - PointLatLng(destinationP.latitude, destinationP.longitude), - travelMode: TravelMode.driving, - ); - if (result.points.isNotEmpty) { - result.points.forEach((PointLatLng point) { - polylineCoordinates.add(LatLng(point.latitude, point.longitude)); - }); - } else { - print(result.errorMessage); - } - return polylineCoordinates; - } - - void generatePolyLineFromPoints(List polylineCoordinates) async { - PolylineId id = const PolylineId("poly"); - Polyline polyline = Polyline( - polylineId: id, - color: Colors.blue, - points: polylineCoordinates, - width: 6, - ); - if (mounted) { - setState(() { - polylines[id] = polyline; - }); - } - } - - void _applyMapStyle(GoogleMapController controller) { - controller.setMapStyle(MAP_STYLE); - } - - Future _animateCameraToPosition(LatLng position) async { - final GoogleMapController controller = await _mapController.future; - controller.animateCamera( - CameraUpdate.newCameraPosition( - CameraPosition( - target: position, - zoom: 17.5, - ), - ), - ); - } - - Future _animateInitialCameraToPosition(String coordsOfCity) async { - List coords = coordsOfCity.split(','); - double lat = double.parse(coords[0]); - double lng = double.parse(coords[1]); - - _animateCameraToPosition(LatLng(lat, lng)); - - try { - Position position = await _determinePosition(); - - _animateCameraToPosition(LatLng(position.latitude, position.longitude)); - - if (mounted) { - setState(() {}); - } - } catch (e) { - print('Error: $e'); - } - } - - void updateMarkersForServiceType(String serviceType) { - getUsersWithActiveStatus(serviceType).then((value) { - markers.clear(); - for (final element in value) { - if (element['latitude'] != null && element['longitude'] != null) { - markers.add( - Marker( - icon: _markerIcon!, - markerId: MarkerId(element['id']?.toString() ?? ''), - position: LatLng( - (element['latitude'] as num).toDouble(), - (element['longitude'] as num).toDouble(), - ), - ), - ); - } - } - }).catchError((e) { - print('Error al actualizar los marcadores: $e'); - }); - } - - Future _determinePosition() async { - bool serviceEnabled; - LocationPermission permission; - - serviceEnabled = await Geolocator.isLocationServiceEnabled(); - - if (!serviceEnabled) { - return Future.error('Location services are disabled'); - } - - permission = await Geolocator.checkPermission(); - - if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); - - if (permission == LocationPermission.denied) { - return Future.error('Location permission denied'); - } - } - - if (permission == LocationPermission.deniedForever) { - return Future.error('Location permissions are permanently denied'); - } - - Position position = await Geolocator.getCurrentPosition(); - - return position; - } - - Future getLocationName(double latitude, double longitude) async { - String address; - try { - List placemarks = - await geocoding.placemarkFromCoordinates(latitude, longitude); - geocoding.Placemark place = placemarks[0]; - - if (place.thoroughfare != '' || place.subThoroughfare != '') { - address = - "${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}"; - } else { - address = ''; - } - } catch (e) { - address = ''; - } - - return address; - } - - void placeAutoComplete(String query) async { - Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", { - "input": query, - "location": _coordsOfCity, - }); - - String? response = await NetworkUtility.fetchUrl(uri); - - if (response != null) { - if (mounted) { - setState(() { - _placesList = jsonDecode(response.toString())['results']; - }); - } - } - } - - // ponytail: getUsersWithActiveStatus was a Firestore map-marker query; silently dropped (markers feature removed) - Future>> getUsersWithActiveStatus( - String serviceType) async { - try { - final query = serviceType != 'Servicio' - ? '/professionals?profession=${Uri.encodeComponent(serviceType)}' - : '/professionals'; - final List data = await ApiService.instance.get(query); - return data.cast>(); - } catch (e) { - print('Error fetching professionals: $e'); - return []; - } - } - - // ponytail: FCM token save removed; not applicable - void _saveToken() {} - - Future _getAppVersion() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - - print('App version: $version'); - - if (mounted) { - setState(() { - appVersion = version; - }); - } - - print('App version: $settings'); - - if (settings != null) { - _checkForUpdate(settings); - } - } - - void _checkForUpdate(String? settings) { - if (appVersion == settings) { - showDialog( - context: context, - builder: (context) { - return WillPopScope( - onWillPop: () async { - return false; - }, - child: Center( - child: AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - title: const Text( - "Debes Actualizar la Aplicación", - style: TextStyle(fontWeight: FontWeight.bold), - ), - content: const Text( - "Tienes una versión desactualizada de nuestra aplicación. Te recomendamos actualizarla para disfrutar de las últimas características y mejoras.", - ), - actions: [ - TextButton( - child: const Text( - "Actualizar", - style: - TextStyle(fontWeight: FontWeight.w600, fontSize: 20), - ), - onPressed: () { - if (Platform.isAndroid) { - launchUrl(_url); - } else { - launchUrl(_urlIos); - } - }, - ), - ], - ), - ), - ); - }, - ); - } - } - - Future _selectDateAndTime(BuildContext context) async { - final DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: now, - firstDate: now, - lastDate: DateTime(now.year + 1), - ); - - if (pickedDate == null) { - return; - } - - final TimeOfDay? pickedTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - - if (pickedTime != null) { - final DateTime selectedDateTime = DateTime( - pickedDate.year, - pickedDate.month, - pickedDate.day, - pickedTime.hour, - pickedTime.minute, - ); - - if (selectedDateTime.isBefore(DateTime.now())) { - WarningSnackbar.show( - title: 'Hora inválida', - message: - 'La hora seleccionada no es valida porque es anterior a la hora actual.', - ); - return; - } - - if (mounted) { - setState(() {}); - } - } - } - - Future sendPushNotification(String pro) async { - try { - http.Response response = await http.post( - Uri.parse('https://fcm.googleapis.com/fcm/send'), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'Authorization': - 'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2', - }, - body: jsonEncode( - { - 'notification': { - 'body': 'alguien a solicitado tus servicios', - 'title': 'Nueva solicitud', - }, - 'priority': 'high', - 'data': { - 'click_action': 'FLUTTER_NOTIFICATION_CLICK', - 'id': '1', - 'status': 'done', - 'screen': 'solicitud' - }, - 'to': pro - }, - ), - ); - - response; - } catch (e) { - print('error al enviar notificacion $e'); - } - } -} diff --git a/lib/src/presentation/screens/messages.dart b/lib/src/presentation/screens/messages.dart deleted file mode 100644 index 0cd2626..0000000 --- a/lib/src/presentation/screens/messages.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:community_material_icon/community_material_icon.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/chat_model.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/presentation/screens/chat.dart'; - -class MessagesScreen extends StatefulWidget { - const MessagesScreen({super.key}); - - @override - State createState() => _MessagesScreenState(); -} - -class _MessagesScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List list = []; - @override - void initState() { - super.initState(); - - ChatModel.getChatsByProId(uid!).then( - (List s) => setState(() { - list = s; - }), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Mensajes'), - body: Column( - children: [ - const Padding( - padding: EdgeInsets.all(10), - child: TextField( - // controller: searchController, - decoration: InputDecoration( - hintText: 'Escribe un nombre', - prefixIcon: Icon(CommunityMaterialIcons.stethoscope), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: list.length, - itemBuilder: (BuildContext context, int index) { - if (list[index].messages.isNotEmpty) { - final user = list[index].user; - final lastMsg = list[index].messages.last; - return ListTile( - title: Text( - (user?.name ?? ''), - ), - subtitle: Text( - '" ${lastMsg.content} "', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - leading: ReferencePhoto( - ref: user?.photo, - size: 55, - sizeCircle: 60, - ), - trailing: const Column( - children: [ - SizedBox(height: 8), - Icon( - Icons.keyboard_arrow_right, - color: Colors.black, - ), - ], - ), - onTap: () { - Event.getEventById(list[index].id).then((value) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ChatScreen(eventoId: list[index].id); - }, - ), - ); - }); - }, - ); - } else { - return const SizedBox(); - } - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/messages_user.dart b/lib/src/presentation/screens/messages_user.dart deleted file mode 100644 index 30f5b61..0000000 --- a/lib/src/presentation/screens/messages_user.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:community_material_icon/community_material_icon.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/chat_model.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/presentation/screens/chat.dart'; - -class MessagesUserScreen extends StatefulWidget { - const MessagesUserScreen({super.key}); - - @override - State createState() => _MessagesUserScreenState(); -} - -class _MessagesUserScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List list = []; - @override - void initState() { - super.initState(); - - ChatModel.getChatsByUserId(uid!).then( - (List s) => setState(() { - list = s; - }), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Mensajes'), - body: Column( - children: [ - const Padding( - padding: EdgeInsets.all(10), - child: TextField( - // controller: searchController, - decoration: InputDecoration( - hintText: 'Escribe un nombre', - prefixIcon: Icon(CommunityMaterialIcons.stethoscope), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: list.length, - itemBuilder: (BuildContext context, int index) { - if (list[index].messages.isNotEmpty) { - final pro = list[index].professional; - final lastMsg = list[index].messages.last; - return ListTile( - title: Text( - (pro?.name ?? ''), - ), - subtitle: Text( - '" ${lastMsg.content} "', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - leading: ReferencePhoto( - ref: pro?.photo, - size: 55, - sizeCircle: 60, - ), - trailing: Column( - children: [ - SizedBox(height: 8), - const Icon( - Icons.keyboard_arrow_right, - color: Colors.black, - ), - Text( - lastMsg.timestamp.day >= DateTime.now().day - ? DateFormat('h:mm a').format(lastMsg.timestamp) - : DateFormat('dd/MM/yyyy', 'es') - .format(lastMsg.timestamp), - style: - const TextStyle(color: Colors.grey, fontSize: 12), - ) - ], - ), - onTap: () { - Event.getEventById(list[index].id).then((value) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ChatScreen(eventoId: list[index].id); - }, - ), - ); - }); - }, - ); - } else { - return const SizedBox(); - } - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/my_services.dart b/lib/src/presentation/screens/my_services.dart deleted file mode 100644 index 7fb0cf6..0000000 --- a/lib/src/presentation/screens/my_services.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/drawer_professional.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/presentation/screens/cita.dart'; -import 'package:prosappco/src/presentation/screens/score.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class MyServicesScreen extends StatefulWidget { - MyServicesScreen({super.key}); - - @override - State createState() => _MyServicesScreenState(); -} - -class _MyServicesScreenState extends State { - DateTime today = DateTime.now(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List eventos = []; - bool loading = true; - - @override - void initState() { - super.initState(); - _load(); - } - - Future _load() async { - try { - final raw = await ApiService.instance.get('/services/me'); - final List data = raw is Map ? (raw['data'] ?? []) : (raw as List); - final List loaded = []; - for (final e in data) { - final event = Event.fromJson(e as Map); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - loaded.add(event); - } - loaded.sort((a, b) { - if (a.timeStamp == null || b.timeStamp == null) return 0; - return a.timeStamp!.compareTo(b.timeStamp!); - }); - if (mounted) setState(() { - eventos = loaded; - loading = false; - }); - } catch (e) { - print('Error loading my services: $e'); - if (mounted) setState(() => loading = false); - } - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Mis servicios'), - drawer: DrawerProfessional(), - body: loading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - child: Column(children: [_eventList(context)]), - ), - ), - ); - } - - Widget _eventList(BuildContext context) { - final filtered = - eventos.where((e) => e.professionalId != e.userId).toList(); - - if (filtered.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Center(child: Text('No tienes citas')), - ); - } - - return FutureBuilder>( - future: _loadProfessionalNames(filtered), - builder: (context, snapshot) { - final names = snapshot.data ?? {}; - return Column( - children: [ - ...filtered.map((event) => ListTile( - tileColor: event.status == 'cancelled' || event.status == 'denied' - ? Colors.red[100] - : Colors.blue[100], - onTap: () { - if (event.status == 'completed') { - if (event.userId == uid) { - if (event.userScored) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => CitaScreen(evento: event), - ), - ); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => - ScoreScreen(evento: event, pro: false), - ), - ); - } - } - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => CitaScreen(evento: event), - ), - ); - } - }, - leading: _statusIcon(event.status), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - names[event.professionalId] ?? 'N/D', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', - style: TextStyle(color: Colors.grey[600], fontSize: 16), - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: event.scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: - const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '"${event.description}"', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - trailing: const Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [Icon(Icons.keyboard_arrow_right)], - ), - )), - ], - ); - }, - ); - } - - Future> _loadProfessionalNames( - List events) async { - final ids = events.map((e) => e.professionalId).toSet(); - final Map names = {}; - for (final id in ids) { - try { - final Map data = - await ApiService.instance.get('/users/$id'); - names[id] = data['name'] ?? 'N/D'; - } catch (_) { - names[id] = 'N/D'; - } - } - return names; - } - - Widget _statusIcon(String status) { - switch (status) { - case 'accepted': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.check, color: Colors.blue, size: 30), - Text('Aceptado', style: TextStyle(fontSize: 12)), - ]); - case 'active': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.access_time, color: Colors.blue, size: 30), - Text('Iniciado', style: TextStyle(fontSize: 12)), - ]); - case 'pending': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.access_time_outlined, color: Colors.blue, size: 30), - Text('Pendiente', style: TextStyle(fontSize: 12)), - ]); - case 'completed': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.rocket_launch, color: Colors.blue, size: 30), - Text('Finalizado', style: TextStyle(fontSize: 12)), - ]); - default: - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.close, color: Colors.red, size: 30), - Text('Cancelado', style: TextStyle(fontSize: 12)), - ]); - } - } -} diff --git a/lib/src/presentation/screens/my_services_pro.dart b/lib/src/presentation/screens/my_services_pro.dart deleted file mode 100644 index 1627abf..0000000 --- a/lib/src/presentation/screens/my_services_pro.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/drawer_professional.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/presentation/screens/cita.dart'; -import 'package:prosappco/src/presentation/screens/score.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class MyServicesProScreen extends StatefulWidget { - MyServicesProScreen({super.key}); - - @override - State createState() => _MyServicesProScreenState(); -} - -class _MyServicesProScreenState extends State { - DateTime today = DateTime.now(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List eventos = []; - bool loading = true; - - @override - void initState() { - super.initState(); - _load(); - } - - Future _load() async { - try { - final raw = await ApiService.instance.get('/services/professional'); - final List data = raw is Map ? (raw['data'] ?? []) : (raw as List); - final List loaded = []; - for (final e in data) { - final event = Event.fromJson(e as Map); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - loaded.add(event); - } - loaded.sort((a, b) { - if (a.timeStamp == null || b.timeStamp == null) return 0; - return a.timeStamp!.compareTo(b.timeStamp!); - }); - if (mounted) setState(() { - eventos = loaded; - loading = false; - }); - } catch (e) { - print('Error loading pro services: $e'); - if (mounted) setState(() => loading = false); - } - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Mis servicios', - ), - drawer: DrawerProfessional(), - body: loading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - child: Column(children: [_eventList(context)]), - ), - ), - ); - } - - Widget _eventList(BuildContext context) { - if (eventos.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Center(child: Text('No tienes citas')), - ); - } - return Column( - children: [ - ...eventos.map( - (event) => ListTile( - tileColor: event.status == 'cancelled' || event.status == 'denied' - ? Colors.red[100] - : Colors.blue[100], - onTap: () { - if (event.status == 'completed') { - if (event.professionalId == uid) { - if (event.professionalScored) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => CitaScreen(evento: event), - ), - ); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => ScoreScreen(evento: event, pro: true), - ), - ); - } - } - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (_) => CitaScreen(evento: event), - ), - ); - } - }, - leading: _statusIcon(event.status), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - event.title, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', - style: TextStyle(color: Colors.grey[600], fontSize: 16), - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: event.scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '"${event.description}"', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - trailing: const Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [Icon(Icons.keyboard_arrow_right)], - ), - ), - ), - ], - ); - } - - Widget _statusIcon(String status) { - switch (status) { - case 'accepted': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.check, color: Colors.blue, size: 30), - Text('Aceptado', style: TextStyle(fontSize: 12)), - ]); - case 'active': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.access_time, color: Colors.blue, size: 30), - Text('Iniciado', style: TextStyle(fontSize: 12)), - ]); - case 'completed': - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.rocket_launch, color: Colors.blue, size: 30), - Text('Finalizado', style: TextStyle(fontSize: 12)), - ]); - default: - return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.close, color: Colors.red, size: 30), - Text('Cancelado', style: TextStyle(fontSize: 12)), - ]); - } - } -} diff --git a/lib/src/presentation/screens/new_number.dart b/lib/src/presentation/screens/new_number.dart deleted file mode 100644 index f3dc576..0000000 --- a/lib/src/presentation/screens/new_number.dart +++ /dev/null @@ -1,261 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:intl_phone_field/intl_phone_field.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; -import 'package:prosappco/src/controllers/new_phone_controller.dart'; - -class NewNumberScreen extends StatefulWidget { - const NewNumberScreen({super.key}); - - @override - State createState() => _NewNumberScreenState(); -} - -class _NewNumberScreenState extends State { - final controller = NewPhoneController.instance; - String completePhoneNumber = ''; - final _formKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - appBar: PopAppbar( - onPressed: () { - _formKey.currentState!.reset(); - Navigator.pop(context); - }, - label: 'Añadir numero', - ), - body: !kIsWeb - ? Container( - padding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 20), - margin: const EdgeInsets.only(top: 30, left: 50, right: 50), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Numero de celular', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - )), - Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: IntlPhoneField( - controller: controller.newPhoneNo, - initialCountryCode: 'CO', - onChanged: (newPhoneNo) { - completePhoneNumber = newPhoneNo.completeNumber; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - ), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 30), - child: Text( - 'Se enviará un código a este número de celular', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13.0, color: Color(0xFF65676B))), - ), - Container( - margin: const EdgeInsets.only(top: 10, bottom: 30), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: const Row( - children: [ - Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - SizedBox(width: 15), - Expanded( - child: Text( - '¡Al actualizar tu número, se cerrará la sesión para confirmar que eres tú!.', - style: - TextStyle(color: Colors.black, fontSize: 14), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Center( - child: PrimaryButtom( - onPressed: () { - controller.updatePhoneNumber( - completePhoneNumber.toString()); - }, - label: 'Actualizar número'), - ), - ), - ], - ), - ) - : Center( - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 20), - width: 400, - margin: const EdgeInsets.only(top: 30, left: 50, right: 50), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Numero de celular', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - )), - Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: IntlPhoneField( - controller: controller.newPhoneNo, - initialCountryCode: 'CO', - onChanged: (newPhoneNo) { - completePhoneNumber = newPhoneNo.completeNumber; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - ), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 30), - child: Text( - 'Se enviará un código a este número de celular', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13.0, color: Color(0xFF65676B))), - ), - Container( - margin: const EdgeInsets.only(top: 10, bottom: 30), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: const Row( - children: [ - Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - SizedBox(width: 15), - Expanded( - child: Text( - '¡Al actualizar tu número, se cerrará la sesión para confirmar que eres tú!.', - style: TextStyle( - color: Colors.black, fontSize: 14), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Center( - child: PrimaryButtom( - onPressed: () { - controller.updatePhoneNumber( - completePhoneNumber.toString()); - }, - label: 'Actualizar número'), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/new_number_validation.dart b/lib/src/presentation/screens/new_number_validation.dart deleted file mode 100644 index f98123e..0000000 --- a/lib/src/presentation/screens/new_number_validation.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; -import 'package:prosappco/src/controllers/otp_controller.dart'; - -class NewNumberValidationScreen extends StatefulWidget { - const NewNumberValidationScreen({super.key}); - - @override - State createState() => - _NewNumberValidationScreenState(); -} - -class _NewNumberValidationScreenState extends State { - dynamic otp; - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: const Color(0xFFD6F4FF), - body: Stack( - children: [ - Container( - margin: const EdgeInsets.only(top: 280), - width: double.infinity, - height: 600, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topRight: Radius.circular(50), - topLeft: Radius.circular(50))), - ), - Container( - margin: const EdgeInsets.only(top: 120, left: 70, right: 70), - child: const Image(image: AssetImage('images/logo_prosapp.png')), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20), - margin: const EdgeInsets.only(top: 280, left: 25), - child: Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - const Text( - 'Valida el código', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.right, - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20), - margin: const EdgeInsets.only(top: 350, left: 50, right: 50), - child: Column(children: [ - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Numero de celular', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Row( - children: [ - const Expanded( - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: '+57', - suffixIcon: Icon(Icons.edit), - ), - ), - ), - TextButton( - onPressed: () { - // Acción a realizar cuando se hace clic en el texto - }, - child: const Text('Reenviar código'), - ), - ], - ), - ), - const Align( - alignment: Alignment.topLeft, - child: Padding( - padding: EdgeInsets.only(bottom: 5), - child: Text('Código', - textAlign: TextAlign.left, - style: - TextStyle(fontSize: 18.0, color: Color(0xFF65676B))), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: OtpTextField( - numberOfFields: 6, - focusedBorderColor: Colors.blue, - fillColor: Colors.black.withOpacity(0.1), - filled: true, - keyboardType: TextInputType.number, - onSubmit: (code) { - otp = code; - OTPController.instance.verifyOTP(otp); - }, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 40), - child: Center( - child: ElevatedButton( - onPressed: () { - OTPController.instance.verifyOTP(otp); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), // Color del botón - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(50), // Bordes redondeados - ), - elevation: 0, - minimumSize: const Size(230, 60), // Tamaño mínimo del botón - ), - child: const Text( - 'Valida el código', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - )), - ), - ]), - ), - ], - ), - )); - } -} diff --git a/lib/src/presentation/screens/new_password.dart b/lib/src/presentation/screens/new_password.dart deleted file mode 100644 index 5955590..0000000 --- a/lib/src/presentation/screens/new_password.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; - -class NewPasswordScreen extends StatefulWidget { - NewPasswordScreen({super.key}); - - @override - State createState() => _NewPasswordScreenState(); -} - -class _NewPasswordScreenState extends State { - final _currentPasswordController = TextEditingController(); - final _newPasswordController = TextEditingController(); - bool _obscureText = true; - bool _obscureText2 = true; - - void updatePassword(String currentPassword, String newPassword) { - // ponytail: password change requires backend endpoint; show support message - showAppSnackBar('Cambio de contraseña', 'Para cambiar tu contraseña contacta a soporte.', color: Colors.grey.shade700); - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: ' Cambia tu contraseña', - ), - body: Center( - child: SizedBox( - width: 300, - child: Padding( - padding: const EdgeInsets.only(top: 20), - child: Column( - children: [ - const Text( - "Ten en cuenta que al cambiar tu contraseña, se cerrará automáticamente tu sesión.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 40), - TextFormField( - controller: _currentPasswordController, - obscureText: _obscureText, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - hintText: 'Contraseña (Actual)'), - ), - const SizedBox(height: 40), - TextFormField( - controller: _newPasswordController, - obscureText: _obscureText2, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText2 - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText2 = !_obscureText2; - }); - }, - ), - hintText: 'Contraseña (Nueva)'), - ), - const SizedBox(height: 80), - PrimaryButtom( - onPressed: () { - updatePassword(_currentPasswordController.text.trim(), - _newPasswordController.text.trim()); - }, - label: 'Actualizar contraseña'), - const SizedBox(height: 30), - const Text( - "Esta contraseña es valida si el inicio de sesión es por email.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/profession.dart b/lib/src/presentation/screens/profession.dart deleted file mode 100644 index 2da80af..0000000 --- a/lib/src/presentation/screens/profession.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:diacritic/diacritic.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -Future> getProfessions() async { - try { - final List data = await ApiService.instance.get('/professions'); - return data.map((e) => (e['name'] ?? e.toString()) as String).toList(); - } catch (e) { - print('$e'); - } - - return []; -} - -class ProfessionScreen extends StatefulWidget { - const ProfessionScreen({super.key}); - - @override - State createState() => _ProfessionScreenState(); -} - -class _ProfessionScreenState extends State { - List? filteredProfessions; - TextEditingController searchController = TextEditingController(); - List? _professions; - final ScrollController _scrollController = ScrollController(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - bool isNewProfessionAdded = false; - - @override - void initState() { - super.initState(); - searchController.addListener(() { - setState(() { - if (_professions != null) { - if (searchController.text.isEmpty) { - filteredProfessions = _professions!; - } else { - filteredProfessions = _professions! - .where((profession) => removeDiacritics(profession) - .toLowerCase() - .contains( - removeDiacritics(searchController.text.toLowerCase()))) - .toList(); - } - } - }); - }); - - if (_professions == null) { - getProfessions().then((List element) => setState(() { - _professions = element; - filteredProfessions = element; - })); - } - } - - Future updateProfession(String profession) async { - try { - await ApiService.instance.patch('/users/me', {'profession': profession}); - } catch (e) { - print('Error al actualizar la profesion: $e'); - } - } - - Future saveProfession(String newProfession) async { - try { - // Add profession via API (POST /professions or use existing endpoint) - // For now just add it locally to the list - setState(() { - _professions ??= []; - _professions!.add(newProfession); - filteredProfessions = List.from(_professions!); - isNewProfessionAdded = true; - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - isNewProfessionAdded = false; - }); - } - }); - }); - } catch (e) { - print('Error al guardar la profesión: $e'); - } - } - - @override - Widget build(BuildContext context) { - if (filteredProfessions == null) { - return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF2BA4EC)), - ), - ); - } - var professions = filteredProfessions!; - - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Seleccione su profesión', - ), - body: Column( - children: [ - GestureDetector( - onTap: () { - String newProfession = ""; - - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text("Agregar una profesión"), - content: TextField( - controller: TextEditingController(), - onChanged: (value) { - newProfession = value; - }, - ), - actions: [ - ElevatedButton( - onPressed: () { - if (newProfession.isNotEmpty) { - saveProfession(newProfession); - Navigator.pop(context); - } - }, - child: const Text("Guardar"), - ), - ], - ); - }, - ); - }, - child: Container( - padding: const EdgeInsets.all(10), - child: const Text( - 'Si no vez tu profesion, presiona aquí', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.blue, - ), - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 10, right: 10, top: 0), - child: TextField( - controller: searchController, - decoration: const InputDecoration( - hintText: 'Busca tu profesión', - prefixIcon: Icon(Icons.assignment_ind_rounded), - ), - ), - ), - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: professions.length, - itemBuilder: (BuildContext context, int index) { - return ListTile( - title: Text( - professions[index], - style: TextStyle( - fontSize: 18.0, - color: isNewProfessionAdded && - index == professions.length - 1 - ? Colors.white - : Colors.black, - ), - ), - tileColor: - isNewProfessionAdded && index == professions.length - 1 - ? Colors.blue - : null, - onTap: () { - updateProfession(professions[index]); - Navigator.pop(context, professions[index]); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/professional.dart b/lib/src/presentation/screens/professional.dart deleted file mode 100644 index ec7e67c..0000000 --- a/lib/src/presentation/screens/professional.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'package:diacritic/diacritic.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/presentation/screens/calendar_pro.dart'; -import 'package:prosappco/src/presentation/screens/professional_info.dart'; -import 'package:prosappco/src/presentation/widgets/shared/loading_item_list.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import '../../models/scores_model.dart'; - -class ProfessionalScreen extends StatefulWidget { - final String profession; - const ProfessionalScreen({ - super.key, - required this.profession, - }); - - @override - State createState() => _ProfessionalScreenState(); -} - -class _ProfessionalScreenState extends State { - SettingModel? settings; - List? filteredProfessionals; - List? _professionals; - TextEditingController searchController = TextEditingController(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - String formatCurrency(int number) { - final formatter = - NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: ''); - return '\$${formatter.format(number)}'; - } - - @override - void initState() { - super.initState(); - - SettingModel.getSettings().then((s) { - if (mounted) setState(() => settings = s); - }); - - searchController.addListener(() { - setState(() { - if (_professionals != null) { - if (searchController.text.isEmpty) { - filteredProfessionals = - _professionals!.where((p) => p.id != uid).toList(); - } else { - filteredProfessionals = _professionals! - .where((p) => - removeDiacritics(p.name) - .toLowerCase() - .contains(removeDiacritics( - searchController.text.toLowerCase())) && - p.id != uid) - .toList(); - } - } - }); - }); - - _loadProfessionals(); - } - - Future _loadProfessionals() async { - try { - final String query = widget.profession.isNotEmpty - ? '/professionals?profession=${Uri.encodeComponent(widget.profession)}' - : '/professionals'; - final raw = await ApiService.instance.get(query); - final List data = raw is Map ? (raw['data'] ?? []) : (raw as List); - - final List loaded = []; - for (final e in data) { - final json = e as Map; - if (json['id'] == uid) continue; - - final List especializaciones = - ((json['especialidades'] ?? json['specialties'] ?? []) as List) - .map((x) => x.toString()) - .toList(); - - final scores = - await ScoresModel.scoreTo(json['id']?.toString(), true, false); - - loaded.add(Professional( - id: json['id']?.toString() ?? '', - photoUrl: json['picture'] ?? json['photo_url'], - name: json['name'] ?? '', - professionName: json['profession'] ?? json['profesion'] ?? '', - cityName: json['city'] ?? '', - professionalEspecializado: especializaciones, - ubicacion: json['ubicacion'] ?? '', - realAddress: json['address'] ?? '', - latitude: (json['latitude'] ?? 0).toDouble(), - longitude: (json['longitude'] ?? 0).toDouble(), - scores: scores, - tarifa: json['rate'] != null - ? double.tryParse(json['rate'].toString())?.toInt() - : json['tarifas'], - token: json['token'], - )); - } - - if (mounted) { - setState(() { - _professionals = loaded; - filteredProfessionals = loaded.where((p) => p.id != uid).toList(); - }); - } - } catch (e) { - print('Error loading professionals: $e'); - if (mounted) setState(() => filteredProfessionals = []); - } - } - - Future _showChoiceDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "A domicilio", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () => Navigator.of(context).pop("domicilio"), - ), - const Divider(color: Colors.black54), - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "En sitio", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () => Navigator.of(context).pop("sitio"), - ), - ], - ), - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - if (filteredProfessionals == null) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Seleccione un profesional', - ), - body: Column( - children: [ - const Padding( - padding: EdgeInsets.all(10), - child: TextField( - readOnly: true, - decoration: InputDecoration( - hintText: 'Escriba un nombre', - prefixIcon: Icon(Icons.assignment_ind_rounded), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: 8, - itemBuilder: (BuildContext context, int index) { - return const LoadingItemList(useCircleAvatar: true); - }, - ), - ), - ], - ), - ), - ); - } - - final professionals = filteredProfessionals!; - - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Seleccione un profesional'), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: TextField( - controller: searchController, - decoration: const InputDecoration( - hintText: 'Escriba un nombre', - prefixIcon: Icon(Icons.assignment_ind_rounded), - ), - ), - ), - professionals.isEmpty - ? Expanded( - child: Padding( - padding: - const EdgeInsets.only(left: 20, right: 20, top: 50), - child: - Text('Aún no tenemos ningun(a) ${widget.profession}'), - )) - : Expanded( - child: ListView.builder( - itemCount: professionals.length, - itemBuilder: (BuildContext context, int index) { - final pro = professionals[index]; - return ListTile( - leading: GestureDetector( - onTap: () { - Navigator.of(context).push(CupertinoPageRoute( - builder: (_) => - ProfessionalInfoScreen(professional: pro), - )); - }, - child: ReferencePhoto( - ref: pro.photoUrl, - sizeCircle: 50, - size: 50, - sizeIcon: 35, - ), - ), - trailing: GestureDetector( - child: const Icon(Icons.keyboard_arrow_right), - onTap: () { - Navigator.of(context).push(CupertinoPageRoute( - builder: (_) => - ProfessionalInfoScreen(professional: pro), - )); - }, - ), - title: RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 15.0, color: Colors.black), - children: [ - TextSpan( - text: '${pro.name}, ', - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - TextSpan( - text: - "${pro.professionName}, ${pro.cityName}", - style: TextStyle(color: Colors.grey[600]), - ), - ], - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - settings?.tarifas == true - ? pro.tarifa == 0 - ? const SizedBox() - : Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(20.0), - color: const Color(0xFFD6F4FF), - ), - child: Text( - formatCurrency(pro.tarifa ?? 0), - style: TextStyle( - color: Colors.grey[850], - fontWeight: FontWeight.w600, - ), - ), - ) - : const SizedBox(), - pro.ubicacion == 'ambos' && - settings?.domicilios == true - ? const Text( - 'Disponibilidad a domicilio y en sitio', - style: TextStyle(color: Colors.blue), - ) - : pro.ubicacion == 'sitio' || - settings?.domicilios == false - ? const Text( - 'Disponibilidad en sitio', - style: TextStyle(color: Colors.blue), - ) - : const Text( - 'Disponibilidad a domicilio', - style: TextStyle(color: Colors.blue), - ), - ], - ), - onTap: () async { - if (pro.ubicacion == 'ambos' && - settings?.domicilios == true) { - _showChoiceDialog(context) - .then((String? value) async { - if (value != null) { - var datos = await Navigator.of(context).push( - CupertinoPageRoute( - builder: (_) => - CalendarProScreen(professional: pro), - ), - ); - if (datos != null) { - datos.add(value); - Navigator.pop(context, datos); - } - } - }); - } else if (pro.ubicacion == 'sitio' || - settings?.domicilios == false) { - var datos = await Navigator.of(context).push( - CupertinoPageRoute( - builder: (_) => - CalendarProScreen(professional: pro), - ), - ); - if (datos != null) { - datos.add('sitio'); - Navigator.pop(context, datos); - } - } else { - Navigator.pop(context, [pro, 'domicilio']); - } - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/professional_direccion.dart b/lib/src/presentation/screens/professional_direccion.dart deleted file mode 100644 index e58d0b9..0000000 --- a/lib/src/presentation/screens/professional_direccion.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:geocoding/geocoding.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; - -class ProfessionalDireccionScreen extends StatefulWidget { - const ProfessionalDireccionScreen({super.key}); - - @override - State createState() => - _ProfessionalDireccionScreenState(); -} - -class _ProfessionalDireccionScreenState - extends State { - final TextEditingController _locationController = TextEditingController(); - final String _locationPosition = ''; - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - late GoogleMapController googleMapController; - - static const CameraPosition initialCameraPosition = CameraPosition( - target: LatLng(7.8939100, -72.5078200), - zoom: 14.4746, - ); - - Set markers = {}; - - Future _determinePosition() async { - bool serviceEnabled; - LocationPermission permission; - - serviceEnabled = await Geolocator.isLocationServiceEnabled(); - - if (!serviceEnabled) { - return Future.error('Location services are disabled'); - } - - permission = await Geolocator.checkPermission(); - - if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); - - if (permission == LocationPermission.denied) { - return Future.error('Location permission denied'); - } - } - - if (permission == LocationPermission.deniedForever) { - return Future.error('Location permissions are permanently denied'); - } - - Position position = await Geolocator.getCurrentPosition(); - - return position; - } - - @override - void initState() { - super.initState(); - } - - late String lat; - late String long; - var coordinates; - - Future getLocationName(double latitude, double longitude) async { - String address; - List placemarks = - await placemarkFromCoordinates(latitude, longitude); - Placemark place = placemarks[0]; - - if (place.thoroughfare != '' || place.subThoroughfare != '') { - address = - "${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}"; - } else { - address = ''; - } - return address; - } - - Future updateAddress( - String addressName, double latitude, double longitude) async { - try { - await ApiService.instance.patch('/users/me', { - 'address': addressName, - 'latitude': latitude, - 'longitude': longitude, - }); - } catch (e) { - print('Error al actualizar la dirección: $e'); - } - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Ubicación'), - backgroundColor: const Color(0xFFD6F4FF), - body: Stack( - children: [ - GoogleMap( - mapType: MapType.normal, - initialCameraPosition: initialCameraPosition, - markers: markers, - zoomControlsEnabled: false, - onMapCreated: (GoogleMapController controller) { - googleMapController = controller; - }, - onCameraIdle: () { - if (coordinates != null) { - getLocationName(coordinates.latitude, coordinates.longitude) - .then((locationName) { - setState(() { - _locationController.text = locationName; - }); - }); - } - }, - onCameraMove: (position) { - setState(() { - coordinates = position.target; - }); - }, - gestureRecognizers: >{ - Factory( - () => EagerGestureRecognizer(), - ), - }, - ), - Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(0, 2), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.only( - left: 35, right: 35, bottom: 15, top: 5), - child: TextFormField( - controller: _locationController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.near_me), - hintText: 'Dirección', - ), - ), - ), - ), - const Positioned( - bottom: 10, - right: 0, - left: 0, - top: 0, - child: Icon( - Icons.location_on, - size: 40, - color: Colors.red, - ), - ), - Positioned( - bottom: 130, - right: 20, - child: FloatingActionButton( - onPressed: () async { - try { - Position position = await _determinePosition(); - - googleMapController.animateCamera( - CameraUpdate.newCameraPosition( - CameraPosition( - target: LatLng( - position.latitude, - position.longitude, - ), - zoom: 17), - ), - ); - setState(() {}); - } catch (e) { - showAppSnackBar('Ubicación desactivada', 'Por favor activa la ubicación de tu teléfono.'); - } - - // markers.clear(); - - // markers.add(Marker( - // markerId: const MarkerId('currentLocation'), - // position: - // LatLng(position.latitude, position.longitude))); - }, - elevation: 0, - child: const Icon( - Icons.gps_fixed, - size: 30, - ), - ), - ), - Positioned( - bottom: 30, - left: 0, - right: 0, - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Align( - alignment: Alignment.center, - child: PrimaryButtom( - onPressed: () { - updateAddress( - _locationController.text, - coordinates.latitude, - coordinates.longitude, - ); - Navigator.pop(context, _locationController.text); - }, - label: 'Guardar'), - ), - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/professional_info.dart b/lib/src/presentation/screens/professional_info.dart deleted file mode 100644 index 8720e03..0000000 --- a/lib/src/presentation/screens/professional_info.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/presentation/screens/reputation.dart'; - -import '../../models/scores_model.dart'; - -class ProfessionalInfoScreen extends StatefulWidget { - Professional professional; - - ProfessionalInfoScreen({super.key, required this.professional}); - - @override - State createState() => _ProfessionalInfoScreenState(); -} - -class _ProfessionalInfoScreenState extends State { - SettingModel? settings; - - String formatCurrency(int number) { - final formatter = - NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: ''); - return '\$${formatter.format(number)}'; - } - - @override - void initState() { - super.initState(); - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - }), - ); - } - - loadPaymentMethods(); - } - - Map paymentMethods = {}; - - String _formatPaymentMethods(Map paymentMethods) { - List enabledMethods = paymentMethods.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - - return enabledMethods.join(', '); - } - - void loadPaymentMethods() { - ApiService.instance.get('/professionals/${widget.professional.id}').then((data) { - if (data is Map) { - setState(() { - paymentMethods = Map.from(data['paymentMethods'] ?? {}); - }); - } - }).catchError((e) { - print('Error loading paymentMethods: $e'); - }); - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: widget.professional.name), - body: SingleChildScrollView( - child: Stack( - children: [ - Column( - children: [ - Container( - width: double.infinity, - height: 140, - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 7, - offset: const Offset(0, 2), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 50), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 5, - ), - Text( - widget.professional.name, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - Text( - widget.professional.professionName, - style: const TextStyle(color: Color(0xFF1688C9)), - ), - widget.professional.getEspecializaciones().isEmpty - ? const SizedBox() - : Text( - 'Especializado/a en ${widget.professional.getEspecializaciones()}', - style: const TextStyle(color: Colors.black54), - ), - Text( - widget.professional.cityName, - ), - ], - ), - ), - widget.professional.ubicacion == 'domicilio' - ? Container( - margin: const EdgeInsets.only( - left: 40, right: 40, top: 20, bottom: 20), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: const Row( - children: [ - Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - SizedBox(width: 15), - Text( - 'Este profesional solo atiende en\nsu dirección de trabajo.', - style: TextStyle( - color: Colors.black, fontSize: 13), - ), - ], - ), - ) - : const SizedBox(), - const SizedBox(height: 10), - settings?.tarifas == true - ? RichText( - text: TextSpan( - children: [ - const TextSpan( - text: 'Tarifa consulta ', - style: TextStyle( - color: Colors.black, - ), - ), - TextSpan( - text: formatCurrency( - widget.professional.tarifa ?? 0), - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ) - : const SizedBox(), - const SizedBox(height: 5), - paymentMethods.containsValue(true) - ? Container( - width: MediaQuery.of(context).size.width * 0.8, - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 12, - color: Colors.blue, - ), - children: [ - const TextSpan( - text: 'Métodos de pago recibidos: ', - style: - TextStyle(fontWeight: FontWeight.normal), - ), - TextSpan( - text: _formatPaymentMethods(paymentMethods), - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - ], - ), - )) - : const SizedBox(), - const SizedBox(height: 5), - const Text( - 'Se unió el 02 de abril del 2023', - style: TextStyle(fontSize: 12), - ), - const SizedBox(height: 15), - Container( - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 3, - blurRadius: 5, - offset: const Offset(0, 3), - ), - ], - ), - child: ListTile( - onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return const ReputationScreen(); - }, - ), - ); - }, - trailing: const Icon(Icons.keyboard_arrow_right, - color: Colors.black), - title: const Text( - 'Reputación', - style: TextStyle(color: Colors.black), - ), - subtitle: Row( - children: [ - RatingBar.builder( - initialRating: widget.professional.scores.average, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: - const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${widget.professional.scores.total.toString()}) ${widget.professional.scores.average.toStringAsFixed(1)}'), - ], - ), - ), - ), - ..._scoresList(widget.professional.scores.details), - const SizedBox( - height: 10, - ), - ], - ), - Container( - padding: const EdgeInsets.only( - top: 110, - left: 10, - ), - child: ReferencePhoto( - ref: widget.professional.photoUrl, - size: 100, - sizeCircle: 100, - sizeIcon: 50, - ), - ) - ], - ), - ), - ), - ); - } - - List _scoresList(List list) { - return list.map((e) => _scoreItem(e)).toList(); - } - - Widget _scoreItem(ScoreDetailModel scoreDetails) { - return ListTile( - onTap: () {}, - leading: ReferencePhoto( - ref: scoreDetails.avatar, - size: 50, - sizeCircle: 50, - sizeIcon: 35, - ), - title: Row( - children: [ - RatingBar.builder( - initialRating: scoreDetails.score, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 22, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - Text( - ' (${scoreDetails.score})', - style: const TextStyle(color: Colors.black54, fontSize: 13), - ) - ], - ), - subtitle: Row( - children: [ - Expanded( - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: '${scoreDetails.name}, ', - style: const TextStyle(fontSize: 15, color: Colors.black), - ), - TextSpan( - text: '"${scoreDetails.comment}"', - style: const TextStyle(fontSize: 15, color: Colors.grey), - ), - ], - ), - ), - ), - ], - )); - } -} diff --git a/lib/src/presentation/screens/professional_profile.dart b/lib/src/presentation/screens/professional_profile.dart deleted file mode 100644 index 1c94586..0000000 --- a/lib/src/presentation/screens/professional_profile.dart +++ /dev/null @@ -1,586 +0,0 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/controllers/info_%20professional.dart'; -import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:prosappco/src/services/select_image_profile.dart'; -import 'package:file_picker/file_picker.dart'; - -class ProfessionalProfileScreen extends StatefulWidget { - const ProfessionalProfileScreen({super.key}); - - @override - State createState() => - ProfessionalProfileScreenState(); -} - -class ProfessionalProfileScreenState extends State { - File? imagen_to_upload; - File? image_cedula; - File? image_certificado; - - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - final _formKey = GlobalKey(); - - final controller = InforProfessionalController.instance; - final _cedulaController = TextEditingController(); - final _especializacionController = TextEditingController(); - List images_especializacion = []; - var _profession = '...'; - String? _photoUrl; - - @override - void initState() { - super.initState(); - final currentUser = AuthenticationRepository.instance.currentUser; - _photoUrl = currentUser?.picture; - _profession = currentUser?.profession ?? '...'; - - if (_photoUrl == null || _profession == '...') { - _loadUserData(); - } - } - - Future _loadUserData() async { - try { - final Map data = - await ApiService.instance.get('/auth/me'); - if (mounted) { - setState(() { - _photoUrl ??= data['picture']; - if (_profession == '...') { - _profession = data['profession'] ?? data['profesion'] ?? '...'; - } - }); - } - } catch (e) { - print('Error loading user data: $e'); - } - } - - Future getPdf() async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['pdf'], - ); - if (result != null) { - return File(result.files.single.path!); - } - return null; - } - - Future?> getPdfs() async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['pdf'], - allowMultiple: true, - ); - if (result != null) { - return result.files.map((f) => File(f.path!)).toList(); - } - return null; - } - - Future _uploadFile(File file) async { - try { - final uri = Uri.parse('${ApiService.baseUrl}/storage/upload'); - final request = http.MultipartRequest('POST', uri); - final token = await ApiService.instance.getToken(); - if (token != null) { - request.headers['Authorization'] = 'Bearer $token'; - } - request.files.add(await http.MultipartFile.fromPath('file', file.path)); - final streamed = await request.send(); - final resp = await http.Response.fromStream(streamed); - if (resp.statusCode >= 200 && resp.statusCode < 300) { - final json = ApiService.instance.parseJson(resp.body); - return json['url'] as String?; - } - } catch (e) { - print('Error uploading file: $e'); - } - return null; - } - - Future _showChoiceDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Tomar foto", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getImage(1); - setState(() => imagen_to_upload = File(imagen[0]!.path)); - Navigator.of(context).pop(); - }, - ), - const Divider(color: Colors.black54), - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Abrir Galería", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getImage(2); - setState(() => imagen_to_upload = File(imagen[0]!.path)); - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - }, - ); - } - - Future _showChoiceDialogCedula(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Abrir Galería", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getPdf(); - if (imagen != null) { - setState(() => image_cedula = File(imagen.path)); - } - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - }, - ); - } - - Future _showChoiceDialogCertificado(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Abrir Galería", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getPdf(); - if (imagen != null) { - setState(() => image_certificado = File(imagen.path)); - } - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - }, - ); - } - - Future _showChoiceDialogEspecializaciones(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Abrir Galería", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final List? images = await getPdfs(); - if (images != null) { - setState(() => images_especializacion = images); - } - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - }, - ); - } - - Future sendInfo() async { - final String cedula = _cedulaController.text.trim(); - final String especializacion = _especializacionController.text.trim(); - final List especializaciones = - especializacion.split(',').map((e) => e.trim()).toList(); - - if (cedula.isEmpty) { - WarningSnackbar.show( - title: 'Te faltan campos!!', - message: 'Por favor, ingresa tu cedula', - ); - return; - } - - if (image_cedula == null) { - WarningSnackbar.show( - title: 'Te faltan archivos!!', - message: 'Por favor, adjunta el documento PDF de tu cedula', - ); - return; - } - - if (_profession.isEmpty || _profession == '...') { - WarningSnackbar.show( - title: 'Te falta elegir una profesión!!', - message: - 'Por favor, elige tu profesión antes de enviar la información.', - ); - return; - } - - if (image_certificado == null) { - WarningSnackbar.show( - title: 'Te faltan archivos!!', - message: 'Por favor, adjunta el documento PDF de tu certificado', - ); - return; - } - - if (imagen_to_upload == null && (_photoUrl == null || _photoUrl!.isEmpty)) { - WarningSnackbar.show( - title: 'Sube una foto de perfil', - message: 'Para continuar debes subir una imagen de perfil', - ); - return; - } - - final Map body = { - 'cedula': cedula, - 'estado': 'revision', - 'especializaciones': especializaciones, - }; - - // Upload profile photo if changed - if (imagen_to_upload != null) { - final url = await _uploadFile(imagen_to_upload!); - if (url != null) body['picture'] = url; - } - - // Upload cedula PDF - final cedulaUrl = await _uploadFile(image_cedula!); - if (cedulaUrl != null) body['imgCedula'] = cedulaUrl; - - // Upload certificado PDF - final certUrl = await _uploadFile(image_certificado!); - if (certUrl != null) body['imgCertificado'] = certUrl; - - // Upload especializaciones PDFs - if (images_especializacion.isNotEmpty) { - final List espUrls = []; - for (final f in images_especializacion) { - final url = await _uploadFile(f); - if (url != null) espUrls.add(url); - } - if (espUrls.isNotEmpty) body['imgEspecializaciones'] = espUrls; - } - - try { - await ApiService.instance.patch('/users/me', body); - Navigator.pushReplacementNamed(context, '/solicitudEnviada'); - } catch (e) { - print('Error sending professional info: $e'); - } - } - - @override - Widget build(BuildContext context) { - String profession = _profession.toString(); - double space = 10; - - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Perfil profesional', - ), - body: SingleChildScrollView( - reverse: true, - child: Center( - child: Column( - children: [ - GestureDetector( - onTap: () => _showChoiceDialog(context), - child: Container( - margin: const EdgeInsets.symmetric(vertical: 25), - child: imagen_to_upload != null - ? LocalPhoto(file: imagen_to_upload!) - : ReferencePhoto( - ref: _photoUrl, - size: 100, - sizeCircle: 100, - ), - ), - ), - Container( - width: 300, - padding: const EdgeInsets.only(top: 0), - child: Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.number, - controller: _cedulaController, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Ingrese una cedula válida'; - } - return null; - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.person_outline), - hintText: 'Cedula (Obligatorio)'), - ), - SizedBox(height: space), - ElevatedButton( - onPressed: () => _showChoiceDialogCedula(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Cedula', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - image_cedula != null - ? Icons.check - : Icons.file_upload_outlined, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - SizedBox(height: space), - TextFormField( - readOnly: true, - onTap: () async { - final String? profesion = - (await Navigator.pushNamed( - context, '/profession')) as String?; - if (profesion != null) { - setState(() => _profession = profesion); - } - }, - decoration: InputDecoration( - prefixIcon: - const Icon(Icons.assignment_ind_rounded), - suffixIcon: const Icon(Icons.arrow_drop_down), - hintStyle: profession == '' - ? const TextStyle() - : const TextStyle(color: Colors.black87), - hintText: profession == '' - ? 'Profesión (Obligatorio)' - : profession), - ), - SizedBox(height: space), - ElevatedButton( - onPressed: () => - _showChoiceDialogCertificado(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Certificado profesional', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - image_certificado != null - ? Icons.check - : Icons.file_upload_outlined, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - SizedBox(height: space), - TextFormField( - controller: _especializacionController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.assignment_ind_rounded), - hintText: 'Especialización'), - ), - SizedBox(height: space), - ElevatedButton( - onPressed: () => - _showChoiceDialogEspecializaciones(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Especialización', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - images_especializacion.isEmpty - ? Icons.file_upload_outlined - : Icons.check, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - ], - ), - ), - ), - Container( - margin: const EdgeInsets.only( - left: 40, right: 40, top: 40, bottom: 0), - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: const Row( - children: [ - Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - SizedBox(width: 15), - Expanded( - child: Text( - 'Si tienes más de una especialidad, por favor, adjunta un archivo con el diploma de respaldo para cada una de ellas y sepáralos por comas. ¡Gracias!', - style: TextStyle(color: Colors.black, fontSize: 14), - ), - ) - ], - ), - ), - Container( - alignment: Alignment.bottomCenter, - margin: const EdgeInsets.only( - top: 80, right: 20, left: 20, bottom: 30), - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - sendInfo(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - maximumSize: const Size(350, 50), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Enviar información', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 17, - ), - ), - SizedBox(width: 15), - Icon( - Icons.send, - color: Colors.white, - size: 20, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/professional_profile_web.dart b/lib/src/presentation/screens/professional_profile_web.dart deleted file mode 100644 index 97c235d..0000000 --- a/lib/src/presentation/screens/professional_profile_web.dart +++ /dev/null @@ -1,524 +0,0 @@ -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view_web.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class ProfessionalProfileWebScreen extends StatefulWidget { - const ProfessionalProfileWebScreen({super.key}); - - @override - State createState() => - _ProfessionalProfileWebScreenState(); -} - -class _ProfessionalProfileWebScreenState - extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - // file bytes (web) - Uint8List? selectedImagInBytes; - Uint8List? imageCedulaBytes; - Uint8List? imageCertificadoBytes; - List imagesEspecializacionsBytes = []; - - // file names for display - String selectedImage = ''; - String selectedCedulaImage = ''; - String selectedCertificadoImage = ''; - - // controllers - final _formKey = GlobalKey(); - final TextEditingController _cedulaController = TextEditingController(); - final TextEditingController _especializacionController = - TextEditingController(); - - bool _isLoading = false; - String _profession = '...'; - String? _photoUrl; - - @override - void initState() { - super.initState(); - final currentUser = AuthenticationRepository.instance.currentUser; - _photoUrl = currentUser?.picture; - _profession = currentUser?.profession ?? '...'; - - if (_photoUrl == null || _profession == '...') { - _loadUserData(); - } - } - - Future _loadUserData() async { - try { - final Map data = - await ApiService.instance.get('/auth/me'); - if (mounted) { - setState(() { - _photoUrl ??= data['picture']; - if (_profession == '...') { - _profession = data['profession'] ?? data['profesion'] ?? '...'; - } - }); - } - } catch (e) { - print('Error loading user data: $e'); - } - } - - Future _uploadBytes(Uint8List bytes, String filename) async { - try { - final uri = Uri.parse('${ApiService.baseUrl}/storage/upload'); - final request = http.MultipartRequest('POST', uri); - final token = await ApiService.instance.getToken(); - if (token != null) { - request.headers['Authorization'] = 'Bearer $token'; - } - request.files.add(http.MultipartFile.fromBytes( - 'file', - bytes, - filename: filename, - )); - final streamed = await request.send(); - final resp = await http.Response.fromStream(streamed); - if (resp.statusCode >= 200 && resp.statusCode < 300) { - final json = ApiService.instance.parseJson(resp.body); - return json['url'] as String?; - } - } catch (e) { - print('Error uploading bytes: $e'); - } - return null; - } - - _selectFile(bool imageFrom) async { - FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); - if (fileResult != null) { - setState(() { - selectedImage = fileResult.files.first.name; - selectedImagInBytes = fileResult.files.first.bytes; - }); - } - } - - _selectFileCedula(bool imageFrom) async { - FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); - if (fileResult != null) { - setState(() { - selectedCedulaImage = fileResult.files.first.name; - imageCedulaBytes = fileResult.files.first.bytes; - }); - } - } - - _selectFileCertificado(bool imageFrom) async { - FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); - if (fileResult != null) { - setState(() { - selectedCertificadoImage = fileResult.files.first.name; - imageCertificadoBytes = fileResult.files.first.bytes; - }); - } - } - - _selectFilesEspecializaciones(bool imageFrom) async { - FilePickerResult? fileResult = - await FilePicker.platform.pickFiles(allowMultiple: true); - try { - if (fileResult != null) { - final List selectedFileBytes = []; - for (var file in fileResult.files) { - if (file.bytes != null) selectedFileBytes.add(file.bytes!); - } - setState(() => imagesEspecializacionsBytes = selectedFileBytes); - } - } catch (e) { - print('$e'); - } - } - - Future sendInfo() async { - setState(() => _isLoading = true); - - final String cedula = _cedulaController.text.trim(); - final String especializacion = _especializacionController.text.trim(); - final List especializaciones = - especializacion.split(',').map((e) => e.trim()).toList(); - - if (cedula.isEmpty) { - showSnackBar('Cedula invalida', 'Ingrese una cedula válida'); - setState(() => _isLoading = false); - return; - } - if (imageCedulaBytes == null) { - showSnackBar('Cedula', 'Ingrese una imagen de su cedula'); - setState(() => _isLoading = false); - return; - } - if (imageCertificadoBytes == null) { - showSnackBar('Certificado', 'Ingrese una imagen de su certificado'); - setState(() => _isLoading = false); - return; - } - - final Map body = { - 'cedula': cedula, - 'estado': 'revision', - 'especializaciones': especializaciones, - }; - - // Upload profile photo if selected - if (selectedImagInBytes != null) { - final url = - await _uploadBytes(selectedImagInBytes!, selectedImage.isNotEmpty ? selectedImage : 'profile.jpg'); - if (url != null) body['picture'] = url; - } - - // Upload cedula - final cedulaUrl = await _uploadBytes(imageCedulaBytes!, - selectedCedulaImage.isNotEmpty ? selectedCedulaImage : 'cedula.pdf'); - if (cedulaUrl != null) body['imgCedula'] = cedulaUrl; - - // Upload certificado - final certUrl = await _uploadBytes(imageCertificadoBytes!, - selectedCertificadoImage.isNotEmpty ? selectedCertificadoImage : 'certificado.pdf'); - if (certUrl != null) body['imgCertificado'] = certUrl; - - // Upload especializaciones - if (imagesEspecializacionsBytes.isNotEmpty) { - final List espUrls = []; - for (int i = 0; i < imagesEspecializacionsBytes.length; i++) { - final url = await _uploadBytes( - imagesEspecializacionsBytes[i], 'especializacion_$i.pdf'); - if (url != null) espUrls.add(url); - } - if (espUrls.isNotEmpty) body['imgEspecializaciones'] = espUrls; - } - - try { - await ApiService.instance.patch('/users/me', body); - Navigator.pushReplacementNamed(context, '/solicitudEnviada'); - } catch (e) { - print('Error sending professional info: $e'); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Error'), - content: const Text('Ocurrió un error al cargar las imágenes.'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Aceptar'), - ), - ], - ); - }, - ); - } - - setState(() => _isLoading = false); - } - - void showSnackBar(String title, String message) { - showAppSnackBar(title, message); - } - - @override - Widget build(BuildContext context) { - String profession = _profession.toString(); - double space = 10; - - return Scaffold( - backgroundColor: const Color(0xFFD6F4FF), - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Perfil profesional'), - body: SingleChildScrollView( - child: Center( - child: SizedBox( - width: 350, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: _isLoading - ? const Padding( - padding: EdgeInsets.symmetric(vertical: 30), - child: CircularProgressIndicator(), - ) - : Column( - children: [ - GestureDetector( - onTap: () => _selectFile(true), - child: Padding( - padding: const EdgeInsets.only(top: 20), - child: selectedImagInBytes != null - ? LocalPhotoWeb(file: selectedImagInBytes) - : ReferencePhotoWeb(ref: _photoUrl), - ), - ), - SizedBox( - width: 300, - child: Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.number, - controller: _cedulaController, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Ingrese una cedula válida'; - } - return null; - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.person_outline), - hintText: 'Cedula (Obligatorio)', - ), - ), - SizedBox(height: space), - ElevatedButton( - onPressed: () => - _selectFileCedula(true), - style: ElevatedButton.styleFrom( - backgroundColor: - const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Text( - 'Cedula', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - imageCedulaBytes != null - ? Icons.check - : Icons.file_upload_outlined, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - SizedBox(height: space), - TextFormField( - readOnly: true, - onTap: () async { - final String? profesion = - (await Navigator.pushNamed( - context, '/profession')) - as String?; - if (profesion != null) { - setState( - () => _profession = profesion); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.assignment_ind_rounded), - suffixIcon: const Icon( - Icons.arrow_drop_down), - hintStyle: profession == '' - ? const TextStyle() - : const TextStyle( - color: Colors.black87), - hintText: profession == '' - ? 'Profesión (Obligatorio)' - : profession), - ), - SizedBox(height: space), - ElevatedButton( - onPressed: () => - _selectFileCertificado(true), - style: ElevatedButton.styleFrom( - backgroundColor: - const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Text( - 'Certificado profesional', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - imageCertificadoBytes != null - ? Icons.check - : Icons.file_upload_outlined, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - SizedBox(height: space), - TextFormField( - controller: _especializacionController, - decoration: const InputDecoration( - prefixIcon: Icon( - Icons.assignment_ind_rounded), - hintText: 'Especialización'), - ), - SizedBox(height: space), - ElevatedButton( - onPressed: () => - _selectFilesEspecializaciones(true), - style: ElevatedButton.styleFrom( - backgroundColor: - const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Text( - 'Especialización', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - imagesEspecializacionsBytes.isEmpty - ? Icons.file_upload_outlined - : Icons.check, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - ], - ), - ), - ), - Container( - margin: const EdgeInsets.only(top: 40), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: const Row( - children: [ - Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - SizedBox(width: 15), - Expanded( - child: Text( - 'Si tienes más de una especialidad, por favor, adjunta un archivo con el diploma de respaldo para cada una de ellas y sepáralos por comas. ¡Gracias!', - style: TextStyle( - color: Colors.black, fontSize: 14), - ), - ), - ], - ), - ), - Container( - alignment: Alignment.bottomCenter, - margin: const EdgeInsets.only( - top: 30, - right: 20, - left: 20, - bottom: 30), - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - sendInfo(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - maximumSize: const Size(350, 50), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Enviar información', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 17, - ), - ), - SizedBox(width: 15), - Icon( - Icons.send, - color: Colors.white, - size: 20, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/professional_revision.dart b/lib/src/presentation/screens/professional_revision.dart deleted file mode 100644 index 9bd2f11..0000000 --- a/lib/src/presentation/screens/professional_revision.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; - -class ProfessionalRevisionScreen extends StatefulWidget { - const ProfessionalRevisionScreen({super.key}); - - @override - State createState() => - _ProfessionalRevisionScreenState(); -} - -class _ProfessionalRevisionScreenState - extends State { - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Perfil profesional', - ), - body: SingleChildScrollView( - child: Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.only(top: 40), - child: Column( - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.access_time, - color: Color(0xFF2BA4EC), - ), - SizedBox(width: 8), - Text('Información en revisión.', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - fontWeight: FontWeight.w500)), - ], - ), - const Image( - image: AssetImage('images/checklist.gif'), - width: 300, - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 40), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: - const Offset(0, 3), // changes position of shadow - ), - ], - ), - padding: - const EdgeInsets.symmetric(vertical: 15, horizontal: 25), - child: const Wrap( - alignment: WrapAlignment.start, // Centra el contenido - children: [ - SizedBox( - width: 350, - child: Row( - children: [ - Expanded( - child: Text( - 'Gracias por proporcionar tu información. Actualmente, estamos revisando tus datos y una vez aprobados, podrás acceder al perfil profesional sin problemas. Te notificaremos tan pronto como tu cuenta esté lista. ¡Gracias por tu paciencia!', - style: TextStyle( - fontSize: 14, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - )); - } -} diff --git a/lib/src/presentation/screens/profile/profile.dart b/lib/src/presentation/screens/profile/profile.dart deleted file mode 100644 index a4e6d90..0000000 --- a/lib/src/presentation/screens/profile/profile.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'dart:io'; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:http/http.dart' as http; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/controllers/add_name_email_city.dart'; -import 'package:prosappco/src/presentation/widgets/profile/birth_date_picker.dart'; -import 'package:prosappco/src/presentation/screens/city.dart'; -import 'package:prosappco/src/presentation/screens/new_number.dart'; -import 'package:prosappco/src/presentation/screens/new_password.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_checkbox.dart'; -import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:prosappco/src/services/select_image_profile.dart'; -import 'package:prosappco/src/presentation/widgets/profile/gender_dropdown.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_html/html.dart' as html; - -class ProfileScreen extends StatefulWidget { - const ProfileScreen({Key? key}) : super(key: key); - - @override - State createState() => _ProfileScreenState(); -} - -class _ProfileScreenState extends State { - File? imagen_to_upload; - Uint8List? webImageBytes; - final DateFormat formatter = DateFormat('dd/MM/yyyy'); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - bool _obscureText = true; - final _formKey = GlobalKey(); - final controller = NameEmailCityController.instance; - final _phoneNumberController = TextEditingController(); - final _nameController = TextEditingController(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - - var _ciudad = '...'; - String? _photoUrl; - String? _email; - String gender = ''; - String genderDb = ''; - DateTime? birthDate; - String birthDateDb = ''; - bool enableLoginWithEmail = false; - - @override - void initState() { - super.initState(); - _loadCurrentUser(); - } - - Future _loadCurrentUser() async { - final currentUser = AuthenticationRepository.instance.currentUser; - if (currentUser != null) { - _nameController.text = currentUser.name; - _emailController.text = currentUser.email ?? ''; - _phoneNumberController.text = currentUser.phoneNumber ?? ''; - _photoUrl = currentUser.picture; - _ciudad = currentUser.city.isNotEmpty ? currentUser.city : '...'; - _email = currentUser.email; - genderDb = currentUser.gender ?? ''; - birthDateDb = currentUser.birthday ?? ''; - } else { - // Fallback: fetch from API - try { - final Map data = - await ApiService.instance.get('/auth/me'); - if (mounted) { - setState(() { - _nameController.text = data['name'] ?? ''; - _emailController.text = data['email'] ?? ''; - _phoneNumberController.text = data['phone'] ?? ''; - _photoUrl = data['picture']; - _ciudad = data['city'] ?? '...'; - _email = data['email']; - genderDb = data['gender'] ?? ''; - birthDateDb = data['birthday'] ?? ''; - }); - } - } catch (e) { - print('Error loading user: $e'); - } - } - } - - Future _uploadImageFile(File image) async { - try { - final uri = - Uri.parse('${ApiService.baseUrl}/storage/upload'); - final request = http.MultipartRequest('POST', uri); - final token = await ApiService.instance.getToken(); - if (token != null) { - request.headers['Authorization'] = 'Bearer $token'; - } - request.files.add(await http.MultipartFile.fromPath('file', image.path)); - final streamed = await request.send(); - final resp = await http.Response.fromStream(streamed); - if (resp.statusCode >= 200 && resp.statusCode < 300) { - final json = ApiService.instance.parseJson(resp.body); - return json['url'] as String?; - } - } catch (e) { - print('Error uploading image: $e'); - } - return null; - } - - Future updateInfo() async { - final Map body = {}; - - final newName = _nameController.text.trim(); - if (newName.isNotEmpty) body['name'] = newName; - if (_ciudad != '...' && _ciudad.isNotEmpty) body['city'] = _ciudad; - if (gender.isNotEmpty) body['gender'] = gender; - if (birthDate != null) body['birthday'] = birthDate.toString(); - - try { - if (imagen_to_upload != null) { - final url = await _uploadImageFile(imagen_to_upload!); - if (url != null) body['picture'] = url; - } - - await ApiService.instance.patch('/users/me', body); - - WarningSnackbar.show( - title: 'Informacion actualizada', - message: 'Tu informacion ha sido actualizada con exito.', - icon: const Icon(Icons.check, color: Colors.white), - backgroundColor: Colors.green, - ); - } catch (e) { - print('Error updating info: $e'); - showAppSnackBar('Error', 'No se pudo actualizar la información.'); - } - } - - Future _showChoiceDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Tomar foto", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getImage(1); - setState(() { - imagen_to_upload = File(imagen[0]!.path); - }); - Navigator.of(context).pop(); - }, - ), - const Divider(color: Colors.black54), - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Abrir Galería", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getImage(2); - setState(() { - imagen_to_upload = File(imagen[0]!.path); - }); - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - String city = _ciudad.toString(); - final userProvider = Provider.of(context); - - return Scaffold( - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Perfil', - ), - body: SingleChildScrollView( - reverse: true, - child: Center( - child: Column( - children: [ - GestureDetector( - onTap: () => _showChoiceDialog(context), - child: Container( - margin: const EdgeInsets.symmetric(vertical: 20), - child: imagen_to_upload != null - ? LocalPhoto(file: imagen_to_upload!) - : ReferencePhoto(ref: _photoUrl), - ), - ), - Container( - width: 300, - padding: const EdgeInsets.only(top: 0), - child: Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - controller: _nameController, - maxLength: 50, - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp(r'\s{2,}')), - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Porfavor ingrese un nombre.'; - } - if (value.trim().length < 5) { - return 'Debe tener al menos 5 caracteres.'; - } - return null; - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.person_outline), - hintText: 'Nombre (Obligatorio)', - ), - ), - const SizedBox(), - TextFormField( - readOnly: true, - onTap: () async { - final String? ciudad = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const CityScreen(); - }, - ), - ) as String?; - - if (ciudad != null) { - setState(() { - _ciudad = ciudad; - }); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.near_me), - suffixIcon: const Icon(Icons.arrow_drop_down), - hintStyle: city == '' - ? const TextStyle() - : const TextStyle(color: Colors.black87), - hintText: city == '' || city == '...' - ? 'Ciudad' - : city, - ), - ), - const SizedBox(height: 20.0), - TextFormField( - controller: _phoneNumberController, - readOnly: true, - onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return const NewNumberScreen(); - }, - ), - ); - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.phone_android), - suffixIcon: Icon(Icons.edit_outlined), - hintText: '+57', - ), - ), - genderDb == '' - ? const SizedBox(height: 20.0) - : const SizedBox(), - genderDb == '' - ? GenderDropdown( - onChanged: (selectedGender) { - setState(() { - gender = selectedGender; - }); - }, - ) - : const SizedBox(), - genderDb == '' - ? const SizedBox(height: 20.0) - : const SizedBox(), - birthDateDb == '' - ? BirthDatePicker( - onDateSelected: (birthDay) { - setState(() { - birthDate = birthDay; - }); - }, - controller: TextEditingController( - text: birthDate == null - ? '' - : formatter.format(birthDate!), - ), - ) - : const SizedBox(), - _email != null && _email != '' - ? const SizedBox(height: 20.0) - : const SizedBox(), - _email != null && _email != '' - ? TextFormField( - readOnly: true, - controller: _emailController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.email_outlined), - hintText: 'Email', - ), - ) - : const SizedBox(), - const SizedBox(height: 20), - ], - ), - ), - ), - Container( - alignment: Alignment.bottomCenter, - margin: const EdgeInsets.only(top: 35), - padding: const EdgeInsets.only(bottom: 30), - child: Column( - children: [ - _email != null && _email != '' - ? PrimaryButton( - onPressed: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return NewPasswordScreen(); - }, - ), - ); - }, - text: 'Cambiar Contraseña', - minWidth: 300, - minHeight: 45, - ) - : const SizedBox(height: 20), - const SizedBox(height: 40), - PrimaryButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - await updateInfo(); - if (kIsWeb) { - html.window.location.reload(); - } - } - await userProvider.updateUserDataAndScores(); - }, - text: 'Guardar', - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/profile/profile_pro.dart b/lib/src/presentation/screens/profile/profile_pro.dart deleted file mode 100644 index fd5a73c..0000000 --- a/lib/src/presentation/screens/profile/profile_pro.dart +++ /dev/null @@ -1,496 +0,0 @@ -import 'dart:io'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:http/http.dart' as http; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/banner_photo.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/schedule_picker.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/presentation/screens/horario.dart'; -import 'package:prosappco/src/presentation/screens/professional_direccion.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_checkbox.dart'; -import 'package:prosappco/src/services/api_service.dart'; -import 'package:prosappco/src/services/select_image_profile.dart'; - -class ProfileProScreen extends StatefulWidget { - const ProfileProScreen({super.key}); - - @override - State createState() => _ProfileProScreenState(); -} - -class _ProfileProScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - final TextEditingController _opcionalAddressController = - TextEditingController(); - final TextEditingController _tarifaController = TextEditingController(); - - File? image_portada; - File? imagen_to_upload; - - bool tarifaValue = false; - bool domicilioValue = false; - bool sitioValue = false; - - String? _bannerUrl; - var _direccion = '...'; - var _ubicacion = '...'; - int _tarifa = 0; - SettingModel? settings; - - Map paymentMethods = { - 'Nequi': false, - 'Transferencia Bancaria': false, - 'Datafono': false, - }; - - Map? _horarios; - - @override - void initState() { - super.initState(); - SettingModel.getSettings() - .then((v) => setState(() => settings = v)); - Schedule.getHorarios(uid.toString()) - .then((data) => setState(() => _horarios = data)); - _loadProData(); - } - - Future _loadProData() async { - try { - final Map data = - await ApiService.instance.get('/auth/me'); - final pro = data['professionals'] as Map?; - - if (mounted) { - setState(() { - _bannerUrl = pro?['banner_picture']; - _direccion = data['address'] ?? '...'; - _ubicacion = pro?['ubicacion'] ?? data['ubicacion'] ?? '...'; - _tarifa = pro?['rate'] != null - ? (double.tryParse(pro!['rate'].toString())?.toInt() ?? 0) - : 0; - - if (_tarifa != 0) { - tarifaValue = true; - _tarifaController.text = _tarifa.toString(); - } - - if (_ubicacion == 'ambos') { - sitioValue = true; - domicilioValue = true; - } else if (_ubicacion == 'sitio') { - sitioValue = true; - } else if (_ubicacion == 'domicilio') { - domicilioValue = true; - } - - final pm = pro?['paymentMethods']; - if (pm is Map) { - paymentMethods = Map.from(pm); - } - - final opcAddr = pro?['opcional_address'] ?? data['opcional_address']; - if (opcAddr != null && opcAddr.toString().isNotEmpty) { - _opcionalAddressController.text = opcAddr.toString(); - } - }); - } - } catch (e) { - print('Error loading pro data: $e'); - } - } - - Future _uploadImageFile(File image) async { - try { - final uri = Uri.parse('${ApiService.baseUrl}/storage/upload'); - final request = http.MultipartRequest('POST', uri); - final token = await ApiService.instance.getToken(); - if (token != null) { - request.headers['Authorization'] = 'Bearer $token'; - } - request.files.add(await http.MultipartFile.fromPath('file', image.path)); - final streamed = await request.send(); - final resp = await http.Response.fromStream(streamed); - if (resp.statusCode >= 200 && resp.statusCode < 300) { - final json = ApiService.instance.parseJson(resp.body); - return json['url'] as String?; - } - } catch (e) { - print('Error uploading banner: $e'); - } - return null; - } - - void createSchedules() async { - if (_horarios == null || _horarios!.isEmpty) { - // Initialize default schedules via API - final Map defaultSchedule = {}; - for (var i = 1; i <= 7; i++) { - defaultSchedule['$i'] = { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null, - }; - } - try { - await ApiService.instance - .patch('/users/me', {'horario': defaultSchedule}); - Navigator.pop(context); - } catch (e) { - print(e); - } - } else { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return HorarioScreen(horarios: _horarios!); - }, - ), - ); - } - } - - Future updateInfo() async { - final Map body = {}; - - if (_opcionalAddressController.text.isNotEmpty) { - body['opcional_address'] = _opcionalAddressController.text; - } - body['rate'] = tarifaValue && _tarifaController.text.isNotEmpty - ? int.parse(_tarifaController.text) - : 0; - - String? ubicacion; - if (settings?.domicilios == false) { - if (sitioValue) { - ubicacion = 'sitio'; - } else { - showAppSnackBar('Elige como vas a dar tu servicio', - 'Selecciona si tu servicio es a domicilio o en tu consultorio.'); - return; - } - } else { - if (domicilioValue && sitioValue) { - ubicacion = 'ambos'; - } else if (domicilioValue) { - ubicacion = 'domicilio'; - } else if (sitioValue) { - ubicacion = 'sitio'; - } else { - showAppSnackBar('Elige como vas a dar tu servicio', - 'Selecciona si tu servicio es a domicilio o en tu consultorio.'); - return; - } - } - body['ubicacion'] = ubicacion; - body['paymentMethods'] = paymentMethods; - - if (imagen_to_upload != null) { - final url = await _uploadImageFile(imagen_to_upload!); - if (url != null) body['banner_picture'] = url; - } - - try { - await ApiService.instance.patch('/users/me', body); - } catch (e) { - print('Error updating pro info: $e'); - } - - if (settings?.domicilios == false && sitioValue == false) { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('¿Dónde vas a dar tu servicio?'), - content: const Text('Si no eliges servicio en sitio, no serás visible para los usuarios.'), - actions: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Entendido'), - ), - ], - ), - ); - } else { - showAppSnackBar('Información actualizada', 'Tu información ha sido actualizada con éxito.', - color: const Color(0xFF2BA4EC)); - Navigator.pop(context); - } - } - - Future _showChoiceDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: SingleChildScrollView( - child: ListBody( - children: [ - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Tomar foto", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getImage(1); - setState(() { - imagen_to_upload = File(imagen[0]!.path); - }); - Navigator.of(context).pop(); - }, - ), - const Divider(color: Colors.black54), - GestureDetector( - child: const Text( - textAlign: TextAlign.center, - "Abrir Galería", - style: TextStyle(color: Color(0xFF2BA4EC)), - ), - onTap: () async { - final imagen = await getImage(2); - setState(() { - imagen_to_upload = File(imagen[0]!.path); - }); - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - }, - ); - } - - void toggleDomicilio(bool newValue) { - setState(() { - domicilioValue = newValue; - if (newValue == false && sitioValue == false) sitioValue = true; - }); - } - - void toggleSitio(bool newValue) { - setState(() { - sitioValue = newValue; - if (newValue == false && domicilioValue == false) domicilioValue = true; - }); - } - - @override - Widget build(BuildContext context) { - String address = _direccion.toString(); - - return Scaffold( - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Perfil profesional'), - body: SingleChildScrollView( - reverse: true, - child: Column( - children: [ - GestureDetector( - onTap: () => _showChoiceDialog(context), - child: imagen_to_upload != null - ? LocalPhoto(file: imagen_to_upload!) - : ReferenceBannerPhoto(ref: _bannerUrl), - ), - if (settings?.tarifas == true) - const Divider(color: Colors.white, height: 12), - if (settings?.tarifas == true) - customSwitch('Tarifa', tarifaValue, (value) { - setState(() => tarifaValue = value); - }), - tarifaValue - ? Padding( - padding: - const EdgeInsets.only(left: 40, right: 40, bottom: 15), - child: Column( - children: [ - TextFormField( - controller: _tarifaController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.attach_money), - hintText: 'COP'), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - ), - ], - ).animate().moveY(duration: const Duration(milliseconds: 100)), - ) - : const SizedBox(), - if (settings?.domicilios == true) - const Divider(color: Colors.white, height: 12), - if (settings?.domicilios == true) - customSwitch( - 'Servicio a domicilio', domicilioValue, toggleDomicilio), - const Divider(), - customSwitch('Servicio en sitio', sitioValue, toggleSitio), - sitioValue - ? Padding( - padding: - const EdgeInsets.only(left: 40, right: 40, bottom: 15), - child: Column( - children: [ - TextFormField( - readOnly: true, - onTap: () async { - final String? direccion = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfessionalDireccionScreen(); - }, - ), - ) as String?; - - if (direccion != null) { - setState(() => _direccion = direccion); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.near_me), - hintStyle: address == '' - ? const TextStyle() - : const TextStyle(color: Colors.black87), - hintText: address == '' ? 'Dirección' : address), - ), - const SizedBox(height: 10), - TextFormField( - controller: _opcionalAddressController, - decoration: const InputDecoration( - hintText: 'Oficina / Piso / Conjunto'), - ), - ], - ).animate().moveY(duration: const Duration(milliseconds: 100)), - ) - : const SizedBox(), - const Divider(), - Padding( - padding: const EdgeInsets.only(left: 30, right: 30, bottom: 30), - child: SizedBox( - width: double.infinity, - child: Column( - children: [ - const Text( - 'Metodos de pago', - style: TextStyle(color: Colors.black, fontSize: 17), - ), - for (var entry in paymentMethods.entries) - PrimaryCheckbox( - text: entry.key, - initialValue: entry.value, - onChanged: (value) { - setState(() => paymentMethods[entry.key] = value); - }, - ), - ], - ), - ), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 30), - child: SizedBox( - width: double.infinity, - child: Text( - 'Horario estandar', - style: TextStyle(color: Colors.black, fontSize: 17), - ), - ), - ), - GestureDetector( - onTap: createSchedules, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: _scheduleTable(context), - ), - ), - const SizedBox(height: 10), - PrimaryButton( - onPressed: updateInfo, - text: 'Guardar', - ), - const SizedBox(height: 20), - ], - ), - ), - ); - } - - Widget _scheduleTable(BuildContext context) { - final days = { - '1': 'Lunes', - '2': 'Martes', - '3': 'Miercoles', - '4': 'Jueves', - '5': 'Viernes', - '6': 'Sabado', - '7': 'Domingo', - }; - return Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: days.entries.map((e) { - return TableRow(children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(e.value))), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?[e.key], context)))), - ]); - }).toList(), - ); - } - - Widget customSwitch( - String text, - bool switchValue, - ValueChanged onChanged, - ) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: SizedBox( - height: 40, - child: Row( - children: [ - Expanded( - child: Text( - text, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - ), - Transform.scale( - scale: 1.2, - child: Switch(value: switchValue, onChanged: onChanged), - ), - ], - ), - ), - ); - } - - String timeList(Schedule? schedule, BuildContext context) { - if (schedule == null || !schedule.habilitado) return 'N/A'; - if (schedule.jornadaContinua) { - return '${schedule.range1Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; - } - return '${schedule.range1Hour1?.format(context)} - ${schedule.range1Hour2?.format(context)}; ${schedule.range2Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; - } -} diff --git a/lib/src/presentation/screens/profile/profile_pro_web.dart b/lib/src/presentation/screens/profile/profile_pro_web.dart deleted file mode 100644 index de36960..0000000 --- a/lib/src/presentation/screens/profile/profile_pro_web.dart +++ /dev/null @@ -1,381 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; -import 'package:prosappco/src/components/schedule_picker.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/presentation/screens/horario.dart'; -import 'package:prosappco/src/presentation/screens/ubicacion.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class ProfileProWebScreen extends StatefulWidget { - const ProfileProWebScreen({super.key}); - - @override - State createState() => _ProfileProWebScreenState(); -} - -class _ProfileProWebScreenState extends State { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - final TextEditingController _opcionalAddressController = - TextEditingController(); - final TextEditingController _tarifaController = TextEditingController(); - final TextEditingController _ubicationController = TextEditingController(); - - double latUser = 0.0; - double lngUser = 0.0; - - bool domicilioValue = true; - bool tarifaValue = false; - bool sitioValue = false; - - var _ubicacion = '...'; - int _tarifa = 0; - SettingModel? settings; - - Map? _horarios; - - @override - void initState() { - super.initState(); - SettingModel.getSettings() - .then((v) => setState(() => settings = v)); - Schedule.getHorarios(uid.toString()) - .then((data) => setState(() => _horarios = data)); - _loadProData(); - } - - Future _loadProData() async { - try { - final Map data = - await ApiService.instance.get('/auth/me'); - final pro = data['professionals'] as Map?; - - if (mounted) { - setState(() { - final address = data['address'] ?? ''; - if (address.isNotEmpty) { - _ubicationController.text = address; - } - - final opcAddr = - pro?['opcional_address'] ?? data['opcional_address'] ?? ''; - if (opcAddr.isNotEmpty) { - _opcionalAddressController.text = opcAddr; - } - - _ubicacion = pro?['ubicacion'] ?? data['ubicacion'] ?? '...'; - if (_ubicacion == 'ambos') { - sitioValue = true; - domicilioValue = true; - } else if (_ubicacion == 'sitio') { - sitioValue = true; - } else if (_ubicacion == 'domicilio') { - domicilioValue = true; - } - - _tarifa = pro?['rate'] != null - ? (double.tryParse(pro!['rate'].toString())?.toInt() ?? 0) - : 0; - if (_tarifa != 0) { - tarifaValue = true; - _tarifaController.text = _tarifa.toString(); - } - }); - } - } catch (e) { - print('Error loading pro data: $e'); - } - } - - Future updateInfo() async { - final Map body = {}; - - if (_ubicationController.text.isNotEmpty) { - body['address'] = _ubicationController.text; - if (latUser != 0.0) { - body['latitude'] = latUser; - body['longitude'] = lngUser; - } - } - if (_opcionalAddressController.text.isNotEmpty) { - body['opcional_address'] = _opcionalAddressController.text; - } - - body['rate'] = tarifaValue && _tarifaController.text.isNotEmpty - ? int.parse(_tarifaController.text) - : 0; - - String? ubicacion; - if (settings?.domicilios == false) { - if (sitioValue) { - ubicacion = 'sitio'; - } else { - showAppSnackBar('Elige como vas a dar tu servicio', - 'Selecciona si tu servicio es a domicilio o en tu consultorio.'); - return; - } - } else { - if (domicilioValue && sitioValue) { - ubicacion = 'ambos'; - } else if (domicilioValue) { - ubicacion = 'domicilio'; - } else if (sitioValue) { - ubicacion = 'sitio'; - } else { - showAppSnackBar('Elige como vas a dar tu servicio', - 'Selecciona si tu servicio es a domicilio o en tu consultorio.'); - return; - } - } - body['ubicacion'] = ubicacion; - - try { - await ApiService.instance.patch('/users/me', body); - } catch (e) { - print('Error updating pro info: $e'); - } - - if (settings?.domicilios == false && sitioValue == false) { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('¿Dónde vas a dar tu servicio?'), - content: const Text('Si no eliges servicio en sitio, no serás visible para los usuarios.'), - actions: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Entendido'), - ), - ], - ), - ); - } else { - showAppSnackBar('Información actualizada', 'Tu información ha sido actualizada con éxito.', - color: const Color(0xFF2BA4EC)); - Navigator.pop(context); - } - } - - void createSchedules() async { - if (_horarios == null || _horarios!.isEmpty) { - final Map defaultSchedule = {}; - for (var i = 1; i <= 7; i++) { - defaultSchedule['$i'] = { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null, - }; - } - try { - await ApiService.instance - .patch('/users/me', {'horario': defaultSchedule}); - Navigator.pop(context); - } catch (e) { - print(e); - } - } else { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return HorarioScreen(horarios: _horarios!); - }, - ), - ); - } - } - - void toggleDomicilio(bool newValue) { - setState(() { - domicilioValue = newValue; - if (newValue == false && sitioValue == false) sitioValue = true; - }); - } - - void toggleSitio(bool newValue) { - setState(() { - sitioValue = newValue; - if (newValue == false && domicilioValue == false) domicilioValue = true; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () => Navigator.pop(context), - label: 'Perfil profesional'), - body: SingleChildScrollView( - reverse: true, - child: Column( - children: [ - if (settings?.tarifas == true) - const Divider(color: Colors.white, height: 12), - if (settings?.tarifas == true) - customSwitch('Tarifa', tarifaValue, (value) { - setState(() => tarifaValue = value); - }), - tarifaValue - ? Padding( - padding: - const EdgeInsets.only(left: 40, right: 40, bottom: 15), - child: Column( - children: [ - TextFormField( - controller: _tarifaController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.attach_money), - hintText: 'COP'), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - ), - ], - ).animate().moveY(duration: const Duration(milliseconds: 100)), - ) - : const SizedBox(), - if (settings?.domicilios == true) - const Divider(color: Colors.white, height: 12), - if (settings?.domicilios == true) - customSwitch( - 'Servicio a domicilio', domicilioValue, toggleDomicilio), - const Divider(), - customSwitch('Servicio en sitio', sitioValue, toggleSitio), - sitioValue - ? Padding( - padding: - const EdgeInsets.only(left: 40, right: 40, bottom: 15), - child: Column( - children: [ - TextFormField( - controller: _ubicationController, - readOnly: true, - onTap: () async { - final List datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const UbicacionScreen(); - }, - ), - ); - - if (datos.length == 3) { - final formattedAddress = datos[0]; - final lat = datos[1]; - final lng = datos[2]; - - setState(() { - _ubicationController.text = formattedAddress; - latUser = lat; - lngUser = lng; - }); - } - }, - decoration: const InputDecoration( - hintText: 'Escribe tu ubicación', - prefixIcon: Icon(Icons.near_me), - ), - ), - const SizedBox(height: 10), - TextFormField( - controller: _opcionalAddressController, - decoration: const InputDecoration( - hintText: 'Oficina / Piso / Conjunto'), - ), - ], - ).animate().moveY(duration: const Duration(milliseconds: 100)), - ) - : const SizedBox(), - const Divider(), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 30), - child: SizedBox( - width: double.infinity, - child: Text( - 'Horario estandar', - style: TextStyle(color: Colors.black, fontSize: 15), - ), - ), - ), - GestureDetector( - onTap: createSchedules, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - for (var entry in { - '1': 'Lunes', - '2': 'Martes', - '3': 'Miercoles', - '4': 'Jueves', - '5': 'Viernes', - '6': 'Sabado', - '7': 'Domingo', - }.entries) - TableRow(children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(entry.value))), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: - Text(timeList(_horarios?[entry.key], context)))), - ]), - ], - ), - ), - ), - PrimaryButtom(onPressed: updateInfo, label: 'Guardar'), - const SizedBox(height: 20), - ], - ), - ), - ); - } - - Widget customSwitch( - String text, bool switchValue, ValueChanged onChanged) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: SizedBox( - height: 40, - child: Row( - children: [ - Expanded( - child: Text( - text, - style: const TextStyle(fontSize: 15, color: Colors.black), - ), - ), - Transform.scale( - scale: 1.2, - child: Switch(value: switchValue, onChanged: onChanged), - ), - ], - ), - ), - ); - } - - String timeList(Schedule? schedule, BuildContext context) { - if (schedule == null || !schedule.habilitado) return 'N/A'; - if (schedule.jornadaContinua) { - return '${schedule.range1Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; - } - return '${schedule.range1Hour1?.format(context)} - ${schedule.range1Hour2?.format(context)}; ${schedule.range2Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; - } -} diff --git a/lib/src/presentation/screens/register/register.dart b/lib/src/presentation/screens/register/register.dart deleted file mode 100644 index 97eec3c..0000000 --- a/lib/src/presentation/screens/register/register.dart +++ /dev/null @@ -1,718 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/bottom_sheet.dart'; -import 'package:prosappco/src/components/column_padding.dart'; -import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; -import 'package:prosappco/src/controllers/register_controller.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:prosappco/src/presentation/screens/web_view.dart'; -import 'package:provider/provider.dart'; -import 'package:responsive_builder/responsive_builder.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class RegisterScreen extends StatefulWidget { - const RegisterScreen({super.key}); - - @override - State createState() => _RegisterScreenState(); -} - -class _RegisterScreenState extends State { - bool _obscureText = true; - final controller = RegisterController.instance; - final _formKey = GlobalKey(); - bool _isChecked = false; - SettingModel? settings; - - void _launchURL(String url) async { - if (await canLaunch(url)) { - await launch(url, forceSafariVC: false, forceWebView: false); - } else { - throw 'No se pudo abrir el enlace $url'; - } - } - - @override - void initState() { - super.initState(); - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - }), - ); - } - } - - @override - Widget build(BuildContext context) { - return ScreenTypeLayout.builder( - mobile: (BuildContext context) => _mobileView(context), - tablet: (BuildContext context) => _mobileView(context), - desktop: (BuildContext context) => _desktopView(context), - ); - } - - Widget _mobileView(BuildContext context) { - bool isIOS = Theme.of(context).platform == TargetPlatform.iOS; - - return BottomSheetExpanded( - horizontalPadding: 10, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - const Text( - 'Registro', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.right, - ), - ], - ), - ColumnPadding( - alineacion: MainAxisAlignment.start, - padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), - children: [ - const SizedBox(), - Form( - key: _formKey, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Nombre', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 18), - child: TextFormField( - controller: controller.name, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Por favor ingresa tu nombre'; - } - return null; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all(Radius.circular(50)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all(Radius.circular(50)), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all(Radius.circular(50)), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all(Radius.circular(50)), - ), - hintText: 'Tu nombre completo', - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: Icon(Icons.person_outline), - hintStyle: TextStyle(color: Colors.grey), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Email', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 18), - child: TextFormField( - controller: controller.email, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Por favor ingresa un email'; - } - final RegExp emailRegExp = - RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegExp.hasMatch(value)) { - return 'Por favor ingresa un email válido'; - } - return null; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - errorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - ), - ), - hintText: 'Hello@gmail.com', - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: Icon(Icons.email_outlined), - hintStyle: TextStyle( - color: Colors.grey, - ), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Password', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 25), - child: TextFormField( - controller: controller.password, - obscureText: _obscureText, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor ingresa una contraseña'; - } - if (value.length <= 6) { - return 'Contraseña muy corta'; - } - return null; - }, - decoration: InputDecoration( - errorBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - border: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - hintText: 'Contraseña', - hintStyle: const TextStyle( - color: Colors.grey, - ), - fillColor: const Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, // Centra horizontalmente - children: [ - Checkbox( - value: _isChecked, - onChanged: (value) { - setState(() { - _isChecked = value!; - }); - }, - ), - GestureDetector( - onTap: () { - if (kIsWeb) { - _launchURL(settings?.terminosCondiciones ?? ''); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Políticas de privacidad', - link: settings?.terminosCondiciones ?? '', - ); - }, - ), - ); - } - }, - child: const Text( - 'Acepto los términos y condiciones.', - style: TextStyle( - color: Color(0xFF65676B), - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 25), - child: Center( - child: PrimaryButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - RegisterController.instance - .registerUser( - controller.email.text.trim(), - controller.password.text.trim(), - controller.name.text.trim(), - ) - .then((value) => - Provider.of(context, listen: false) - .initUserProvider()); - } - }, - text: 'Registrarme', - isEnabled: _isChecked, - ), - ), - ), - RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 16.0, - color: Color(0xFF65676B), - fontFamily: 'Poppins', - ), - children: [ - const TextSpan(text: 'Ya estas registrado? '), - WidgetSpan( - child: GestureDetector( - onTap: () { - Navigator.pushReplacementNamed(context, '/login'); - }, - child: const Text( - 'Iniciar Sesión', - style: TextStyle( - fontSize: 16.0, - color: Color(0xFF2BA4EC), - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ) - ], - ), - ], - ); - } - - Widget _desktopView(BuildContext context) { - double height = MediaQuery.of(context).size.height; - double width = MediaQuery.of(context).size.width; - return Scaffold( - backgroundColor: const Color(0xFFD6F4FF), - body: SizedBox( - height: height, - width: width, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: SizedBox( - height: height, - child: const Center( - child: Image( - image: AssetImage('images/logo_prosapp.png'), - ), - ), - ), - ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: width * 0.07), - color: Colors.white, - height: height, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 0, vertical: 20), - child: Row( - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back, - size: 30, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - SizedBox(width: width * 0.01), - const Text( - 'Registro', - style: TextStyle( - color: Color(0xFF262626), - fontSize: 30.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.right, - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 0, vertical: 20), - child: Column( - children: [ - Form( - key: _formKey, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Nombre', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 18), - child: TextFormField( - controller: controller.name, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Por favor ingresa tu nombre'; - } - return null; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all(Radius.circular(50))), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all(Radius.circular(50))), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all(Radius.circular(50))), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all(Radius.circular(50))), - hintText: 'Tu nombre completo', - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: Icon(Icons.person_outline), - hintStyle: TextStyle(color: Colors.grey), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Email', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 18), - child: TextFormField( - controller: controller.email, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Por favor ingresa un email'; - } - final RegExp emailRegExp = RegExp( - r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegExp.hasMatch(value)) { - return 'Por favor ingresa un email válido'; - } - return null; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB( - 255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - hintText: 'Hello@gmail.com', - fillColor: - Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: Icon(Icons.email_outlined), - hintStyle: TextStyle( - color: Colors.grey, - ), - ), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Password', - style: TextStyle( - fontSize: 18.0, - color: Color(0xFF65676B))), - )), - Padding( - padding: const EdgeInsets.only(bottom: 25), - child: TextFormField( - controller: controller.password, - obscureText: _obscureText, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor ingresa una contraseña'; - } - if (value.length <= 6) { - return 'Contraseña muy corta'; - } - return null; - }, - decoration: InputDecoration( - errorBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromARGB( - 255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - border: const OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - hintText: 'Contraseña', - hintStyle: const TextStyle( - color: Colors.grey, - ), - fillColor: const Color.fromARGB( - 255, 239, 239, 239), - filled: true, - prefixIcon: - const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment - .center, // Centra horizontalmente - children: [ - Checkbox( - value: _isChecked, - onChanged: (value) { - setState(() { - _isChecked = value!; - }); - }, - ), - GestureDetector( - onTap: () { - if (kIsWeb) { - _launchURL( - settings?.terminosCondiciones ?? ''); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Políticas de privacidad', - link: settings - ?.terminosCondiciones ?? - '', - ); - }, - ), - ); - } - }, - child: const Text( - 'Acepto los términos y condiciones.', - style: TextStyle( - color: Color(0xFF65676B), - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 25), - child: Center( - child: PrimaryButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - RegisterController.instance - .registerUser( - controller.email.text.trim(), - controller.password.text.trim(), - controller.name.text.trim(), - ) - .then((value) => - Provider.of(context, - listen: false) - .initUserProvider()); - } - }, - text: 'Registrarme', - isEnabled: _isChecked, - )), - ), - RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 16.0, - color: Color(0xFF65676B), - fontFamily: 'Poppins', - ), - children: [ - const TextSpan(text: 'Ya estas registrado? '), - WidgetSpan( - child: GestureDetector( - onTap: () { - Navigator.pushReplacementNamed( - context, '/login'); - }, - child: const Text( - 'Iniciar Sesión', - style: TextStyle( - fontSize: 16.0, - color: Color(0xFF2BA4EC), - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ) - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/reputacion_pro.dart b/lib/src/presentation/screens/reputacion_pro.dart deleted file mode 100644 index 3b58482..0000000 --- a/lib/src/presentation/screens/reputacion_pro.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/scores_model.dart'; - -class ReputationProScreen extends StatefulWidget { - const ReputationProScreen({ - super.key, - }); - - @override - State createState() => _ReputationProScreenState(); -} - -class _ReputationProScreenState extends State { - ScoresModel? scoresModel; - - @override - void initState() { - super.initState(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - if (scoresModel == null) { - ScoresModel.scoreTo(uid.toString(), true, true).then( - (ScoresModel s) => setState(() => scoresModel = s), - ); - } - } - - Widget _scoreList(List list) { - if (list.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 40), - child: Center( - child: Text('Sin calificaciones'), - ), - ); - } else { - return Column( - children: list.map((e) => _scoreItem(e)).toList(), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Reputación'), - body: SingleChildScrollView( - child: _scoreList(scoresModel?.details ?? []), - ), - ); - } - - Widget _scoreItem(ScoreDetailModel scoreDetails) { - return ListTile( - onTap: () {}, - leading: ReferencePhoto( - ref: scoreDetails.avatar, - size: 50, - sizeCircle: 50, - sizeIcon: 35, - ), - title: Row( - children: [ - RatingBar.builder( - initialRating: scoreDetails.score, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 22, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - Text( - ' (${scoreDetails.score})', - style: const TextStyle(color: Colors.black54, fontSize: 13), - ) - ], - ), - subtitle: Row( - children: [ - Expanded( - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: '${scoreDetails.name}, ', - style: const TextStyle(fontSize: 15, color: Colors.black), - ), - TextSpan( - text: '"${scoreDetails.comment}"', - style: const TextStyle(fontSize: 15, color: Colors.grey), - ), - ], - ), - ), - ), - ], - )); - } -} diff --git a/lib/src/presentation/screens/reputation.dart b/lib/src/presentation/screens/reputation.dart deleted file mode 100644 index e37c28b..0000000 --- a/lib/src/presentation/screens/reputation.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/scores_model.dart'; - -class ReputationScreen extends StatefulWidget { - const ReputationScreen({ - super.key, - }); - - @override - State createState() => _ReputationScreenState(); -} - -class _ReputationScreenState extends State { - ScoresModel? scoresModel; - - @override - void initState() { - super.initState(); - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - if (scoresModel == null) { - ScoresModel.scoreTo(uid.toString(), false, true).then( - (ScoresModel s) => setState(() => scoresModel = s), - ); - } - } - - Widget _scoreList(List list) { - if (list.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 40), - child: Center( - child: Text('Sin calificaciones'), - ), - ); - } else { - return Column( - children: list.map((e) => _scoreItem(e)).toList(), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Reputación'), - body: SingleChildScrollView( - child: _scoreList(scoresModel?.details ?? []), - ), - ); - } - - Widget _scoreItem(ScoreDetailModel scoreDetails) { - return ListTile( - onTap: () {}, - leading: ReferencePhoto( - ref: scoreDetails.avatar, - size: 50, - sizeCircle: 50, - sizeIcon: 35, - ), - title: Row( - children: [ - RatingBar.builder( - initialRating: scoreDetails.score, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 22, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - Text( - ' (${scoreDetails.score})', - style: const TextStyle(color: Colors.black54, fontSize: 13), - ) - ], - ), - subtitle: Row( - children: [ - Expanded( - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: '${scoreDetails.name}, ', - style: const TextStyle(fontSize: 15, color: Colors.black), - ), - TextSpan( - text: '"${scoreDetails.comment}"', - style: const TextStyle(fontSize: 15, color: Colors.grey), - ), - ], - ), - ), - ), - ], - )); - } -} diff --git a/lib/src/presentation/screens/request_sent.dart b/lib/src/presentation/screens/request_sent.dart deleted file mode 100644 index ddb9e12..0000000 --- a/lib/src/presentation/screens/request_sent.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; - -class RequestSentScreen extends StatelessWidget { - const RequestSentScreen({super.key}); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pushReplacementNamed(context, '/servicio'); - }, - label: 'Solicitud enviada', - ), - body: Center( - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(top: 30, bottom: 30), - child: Icon( - Icons.check_circle_outline, - size: 35, - color: Color(0xFF35A8ED), - ), - ), - const Padding( - padding: EdgeInsets.only(bottom: 0, left: 30, right: 30), - child: Text( - 'Información enviada con éxito.', - textAlign: TextAlign.center, - style: TextStyle(color: Color(0xFF2BA4EC), fontSize: 20), - ), - ), - Container( - margin: const EdgeInsets.only( - left: 40, right: 40, top: 50, bottom: 100), - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: const SizedBox( - width: 350, - child: Row( - children: [ - Expanded( - child: Text( - "¡Gracias por suministrar tu información! Revisaremos los datos proporcionados y, una vez confirmados, podrás convertirte en un profesional registrado en ProsApp. ¡Esperamos contar contigo pronto!", - style: TextStyle(color: Colors.black, fontSize: 14), - ), - ) - ], - ), - ), - ), - const Expanded(child: SizedBox()), - PrimaryButtom( - onPressed: () { - Navigator.pushReplacementNamed(context, '/servicio'); - }, - label: 'Inicio', - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/reset_password/reset_password.dart b/lib/src/presentation/screens/reset_password/reset_password.dart deleted file mode 100644 index d52f486..0000000 --- a/lib/src/presentation/screens/reset_password/reset_password.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/components/primary_btn.dart'; -import 'package:prosappco/src/controllers/login_email_controller.dart'; - -class ResetPasswordScreen extends StatelessWidget { - const ResetPasswordScreen({super.key}); - - @override - Widget build(BuildContext context) { - final controller = LoginEmailController.instance; - - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Restablecer Contraseña'), - body: SafeArea( - child: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Center( - child: Container( - padding: const EdgeInsets.all(15), - color: Colors.transparent, - width: MediaQuery.of(context).size.width * 0.9, - child: Column( - children: [ - const SizedBox(height: 20), - const Text( - 'Restablecer contraseña', - style: TextStyle(fontWeight: FontWeight.w800, fontSize: 25), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - const Text( - 'Ingresa ingresa tu correo electrónico y te enviaremos un enlace para restablecer tu contraseña', - style: TextStyle(color: Colors.grey, fontSize: 12), - textAlign: TextAlign.center, - ), - const SizedBox(height: 40), - const Padding( - padding: EdgeInsets.only(bottom: 5), - child: Align( - alignment: Alignment.topLeft, - child: Text('Email', - style: TextStyle( - fontSize: 18.0, color: Color(0xFF65676B))), - ), - ), - TextFormField( - controller: controller.email, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingresa un Email'; - } - final RegExp emailRegExp = - RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegExp.hasMatch(value)) { - return 'Por favor, ingresa un Email válido'; - } - return null; - }, - decoration: const InputDecoration( - border: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Color(0xFFECECEC)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - errorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Color.fromARGB(255, 184, 0, 0)), - borderRadius: BorderRadius.all( - Radius.circular(50), - )), - hintText: 'Hello@gmail.com', - fillColor: Color.fromARGB(255, 239, 239, 239), - filled: true, - prefixIcon: Icon(Icons.email_outlined), - hintStyle: TextStyle( - color: Colors.grey, - ), - ), - ), - const SizedBox(height: 80), - PrimaryButtom( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Funcionalidad no disponible. Contacta a soporte.'), - ), - ); - Navigator.pop(context); - }, - label: 'Enviar'), - ], - ), - ), - ), - )), - ); - } -} diff --git a/lib/src/presentation/screens/score.dart b/lib/src/presentation/screens/score.dart deleted file mode 100644 index 4ecaf52..0000000 --- a/lib/src/presentation/screens/score.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class ScoreScreen extends StatefulWidget { - final Event evento; - final bool pro; - const ScoreScreen({super.key, required this.evento, required this.pro}); - - @override - State createState() => ScoreScreenState(); -} - -class ScoreScreenState extends State { - TextEditingController commentController = TextEditingController(); - String? photoUrl; - String nombre = ''; - double _rating = 1.0; - final myUid = AuthenticationRepository.instance.getCurrentUserUid(); - - @override - void initState() { - super.initState(); - _loadUserInfo(); - } - - Future _loadUserInfo() async { - final targetId = (myUid != widget.evento.userId) - ? widget.evento.userId - : widget.evento.professionalId; - try { - final Map data = - await ApiService.instance.get('/users/$targetId'); - if (mounted) { - setState(() { - nombre = data['name'] ?? ''; - photoUrl = data['picture']; - }); - } - } catch (e) { - print('Error loading user for score: $e'); - } - } - - Future _submit() async { - try { - final bool isPro = widget.pro; - // Backend sets professional_scored/user_scored automatically on POST /comments - await ApiService.instance.post('/comments', { - 'service_id': widget.evento.id, - 'destination_id': isPro - ? widget.evento.userId - : widget.evento.professionalId, - 'content': commentController.text, - 'score': _rating, - 'is_from_user': !isPro, - }); - if (mounted) Navigator.pop(context); - } catch (e) { - print('Error submitting score: $e'); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Puntuación'), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: ListTile( - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: ReferencePhoto( - ref: photoUrl, - size: 50, - sizeCircle: 50, - sizeIcon: 35, - ), - ), - title: Text( - nombre, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.w600, - fontSize: 16), - ), - subtitle: Text( - '${DateFormat('dd MMM', 'es').format(DateTime.parse(widget.evento.day))} ${TimeOfDay.fromDateTime(DateTime.parse(widget.evento.range1Hour1)).format(context)}'), - ), - ), - RatingBar.builder( - initialRating: _rating, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 40, - glow: false, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 5), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) { - setState(() { - _rating = rating; - }); - }, - ignoreGestures: false, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Comentario', - style: TextStyle(fontSize: 18), - ), - TextFormField( - maxLines: null, - maxLength: 250, - keyboardType: TextInputType.multiline, - controller: commentController, - ), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 30), - child: ElevatedButton( - onPressed: _submit, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Enviar', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/service_after.dart b/lib/src/presentation/screens/service_after.dart deleted file mode 100644 index c151e60..0000000 --- a/lib/src/presentation/screens/service_after.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/presentation/screens/map/service.dart'; -import 'package:prosappco/src/presentation/screens/service_web.dart'; - -class ServiceAfterScreen extends StatefulWidget { - var eventoId; - ServiceAfterScreen({super.key, this.eventoId}); - - @override - State createState() => _ServiceAfterScreenState(); -} - -class _ServiceAfterScreenState extends State { - Event? evento; - UserModel? user; - ScoresModel? scoresModel; - SettingModel? settings; - - @override - void initState() { - super.initState(); - - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - }), - ); - } - - Event.getEventById(widget.eventoId).then((value) { - setState(() { - evento = value; - }); - if (scoresModel == null) { - ScoresModel.scoreTo(value.professionalId, true, false).then( - (ScoresModel s) => setState(() { - scoresModel = s; - }), - ); - } - UserModel.getUser(value.professionalId).then((s) { - setState( - () => user = s, - ); - }); - }); - } - - String formatCurrency(int number) { - final formatter = - NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: ''); - return '\$${formatter.format(number)}'; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - kIsWeb - ? Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ServiceWebScreen(); - }, - ), - ) - : Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ServiceScreen(); - }, - ), - ); - }, - label: 'Servicio'), - body: Column( - children: [ - ListTile( - leading: ReferencePhoto( - ref: user?.picture, - size: 55, - sizeCircle: 60, - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${user?.name}', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(evento?.day ?? '2023-01-01 00:00:00.000Z'))} ${evento?.range1Hour1 != null ? DateFormat('h:mm a').format(DateTime.parse(evento!.range1Hour1)) : ''}', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), - ), - ], - ), - subtitle: Column( - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${scoresModel?.total.toString()}) ${scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - ], - ), - ), - Container( - margin: - const EdgeInsets.only(left: 40, right: 40, top: 20, bottom: 20), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: Row( - children: [ - const Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - const SizedBox(width: 15), - evento?.ubicacion != 'sitio' - ? const Text( - 'Servicio a su domicilio.', - style: TextStyle(color: Colors.black, fontSize: 14), - ) - : const Text( - 'Servicio en sitio / consultorio', - style: TextStyle(color: Colors.black, fontSize: 14), - ), - ], - ), - ), - settings?.tarifas == true && evento?.tarifa != 0 - ? Column(children: [ - Text( - formatCurrency(evento?.tarifa ?? 0), - style: const TextStyle( - fontWeight: FontWeight.w600, fontSize: 25), - ), - const Text('Tarifa consulta'), - const SizedBox(height: 10) - ]) - : const SizedBox(), - ListTile( - leading: const Icon(Icons.near_me), - title: Text( - '${evento?.address}', - style: TextStyle(fontSize: 15, color: Colors.grey[600]), - ), - ), - Text( - '"${evento?.description}"', - style: - TextStyle(color: Colors.grey[600], fontStyle: FontStyle.italic), - ), - const Center( - child: Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Icon( - Icons.check_circle_outline_rounded, - color: Color(0xFF35A8ED), - size: 70, - ), - ), - Text( - 'Servicio solicitado exitosamente', - style: TextStyle( - color: Color(0xFF35A8ED), - fontSize: 17, - fontWeight: FontWeight.w600), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/service_type.dart b/lib/src/presentation/screens/service_type.dart deleted file mode 100644 index ed4a247..0000000 --- a/lib/src/presentation/screens/service_type.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:diacritic/diacritic.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/presentation/screens/profession.dart'; - -class ServiceTypeScreen extends StatefulWidget { - const ServiceTypeScreen({super.key}); - - @override - State createState() => _ServiceTypeScreenState(); -} - -class _ServiceTypeScreenState extends State { - List? filteredProfessions; - TextEditingController searchController = TextEditingController(); - List? _professions; - - @override - void initState() { - super.initState(); - searchController.addListener(() { - setState(() { - if (_professions != null) { - if (searchController.text.isEmpty) { - filteredProfessions = _professions!; - } else { - filteredProfessions = _professions! - .where((profession) => removeDiacritics(profession) - .toLowerCase() - .contains( - removeDiacritics(searchController.text.toLowerCase()))) - .toList(); - } - } - }); - }); - - if (_professions == null) { - getProfessions().then((List element) => setState(() { - _professions = element; - filteredProfessions = element; - })); - } - } - - @override - Widget build(BuildContext context) { - if (filteredProfessions == null) { - return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF2BA4EC)), - ), - ); - } - List professions = filteredProfessions!; - - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Tipo de servicio'), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: TextField( - controller: searchController, - decoration: const InputDecoration( - hintText: 'Escribe el tipo de servicio', - prefixIcon: Icon(Icons.assignment_ind_rounded), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: professions.length, - itemBuilder: (BuildContext context, int index) { - return ListTile( - title: Text( - professions[index], - style: const TextStyle( - fontSize: 18.0, - color: Colors.black, - ), - ), - onTap: () { - Navigator.pop(context, professions[index]); - }, - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/service_web.dart b/lib/src/presentation/screens/service_web.dart deleted file mode 100644 index 5adda13..0000000 --- a/lib/src/presentation/screens/service_web.dart +++ /dev/null @@ -1,447 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:geocoding/geocoding.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile.dart'; -import 'package:prosappco/src/presentation/widgets/shared/drawer_menu.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/presentation/screens/professional.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/presentation/screens/service_after.dart'; -import 'package:prosappco/src/presentation/screens/ubicacion.dart'; -import 'package:prosappco/src/components/network_utility.dart'; -import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:provider/provider.dart'; - -class ServiceWebScreen extends StatefulWidget { - const ServiceWebScreen({super.key}); - - @override - State createState() => _ServiceOldScreenState(); -} - -class _ServiceOldScreenState extends State { - EventoService eventoService = EventoService(); - String ubicacion = ''; - final TextEditingController _locationController = TextEditingController(); - final TextEditingController _ubicationController = TextEditingController(); - final TextEditingController _profesionalController = TextEditingController(); - final TextEditingController _serviceTypeController = TextEditingController(); - final TextEditingController _observacionController = TextEditingController(); - String professionalId = ''; - String professionalToken = ''; - int? professionalTarifa; - String professionalAddress = ''; - String professionalUbicacion = ''; - double? professionalLatitude; - double? professionalLongitude; - String _serviceType = 'Servicio'; - final DateFormat formatter = DateFormat('dd/MM/yyyy'); - final DateTime now = DateTime.now(); - List _placesList = []; - String _coordsOfCity = '0.0,0.0'; - - DateTime? _selectedDate; - TimeOfDay? _selectedTime; - - GoogleMapController? googleMapController; - Set markers = {}; - - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - BitmapDescriptor? _markerIcon; - String _ciudad = '...'; - - Future _determinePosition() async { - bool serviceEnabled; - LocationPermission permission; - - serviceEnabled = await Geolocator.isLocationServiceEnabled(); - if (!serviceEnabled) return Future.error('Location services are disabled'); - - permission = await Geolocator.checkPermission(); - if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); - if (permission == LocationPermission.denied) { - return Future.error('Location permission denied'); - } - } - if (permission == LocationPermission.deniedForever) { - return Future.error('Location permissions are permanently denied'); - } - - return Geolocator.getCurrentPosition(); - } - - Future _selectDate(BuildContext context) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: now, - firstDate: now, - lastDate: DateTime(now.year + 1), - ); - - if (picked != null && picked != _selectedDate) { - if (mounted) setState(() => _selectedDate = picked); - } - } - - Future _selectTime(BuildContext context) async { - final TimeOfDay? pickedTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (pickedTime != null) { - if (mounted) setState(() => _selectedTime = pickedTime); - } - } - - void _setInitialCameraPosition(String coordsOfCity) async { - List coords = coordsOfCity.split(','); - double lat = double.parse(coords[0]); - double lng = double.parse(coords[1]); - - googleMapController?.moveCamera( - CameraUpdate.newLatLngZoom(LatLng(lat, lng), 14.4746), - ); - - try { - Position position = await _determinePosition(); - googleMapController?.animateCamera( - CameraUpdate.newCameraPosition( - CameraPosition( - target: LatLng(position.latitude, position.longitude), - zoom: 17, - ), - ), - ); - if (mounted) setState(() {}); - } catch (e) { - print('Error: $e'); - } - } - - @override - void dispose() { - _locationController.dispose(); - _ubicationController.dispose(); - _profesionalController.dispose(); - _serviceTypeController.dispose(); - _observacionController.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - - // Load city from in-memory user - final currentUser = AuthenticationRepository.instance.currentUser; - _ciudad = currentUser?.city ?? '...'; - - BitmapDescriptor.fromAssetImage( - const ImageConfiguration(size: Size(6, 6)), 'images/pro_marke.png') - .then((icon) { - if (mounted) setState(() => _markerIcon = icon); - }); - } - - static CameraPosition initialCameraPosition = const CameraPosition( - target: LatLng(7.8939100, -72.5078200), - zoom: 14.4746, - ); - - double latUser = 0.0; - double lngUser = 0.0; - var coordinates; - - Future getLocationName(double latitude, double longitude) async { - List placemarks = - await placemarkFromCoordinates(latitude, longitude); - Placemark place = placemarks[0]; - if (place.thoroughfare != '' || place.subThoroughfare != '') { - return "${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}"; - } - return ''; - } - - void placeAutoComplete(String query) async { - Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", { - "input": query, - "location": _coordsOfCity, - }); - - String? response = await NetworkUtility.fetchUrl(uri); - if (response != null) { - if (mounted) { - setState(() { - _placesList = jsonDecode(response.toString())['results']; - }); - } - } - } - - @override - Widget build(BuildContext context) { - final userProvider = Provider.of(context); - UserModel? user = userProvider.user; - - return Scaffold( - backgroundColor: const Color(0xFFD6F4FF), - drawer: DrawerMenu(), - appBar: AppBar( - elevation: 0, - title: const Text( - 'Prosapp', - style: TextStyle(color: Colors.white, fontSize: 20), - )), - body: Center( - child: SizedBox( - width: 300, - height: 470, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextFormField( - controller: _profesionalController, - readOnly: true, - onTap: () async { - final dynamic datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ProfessionalScreen( - profession: _serviceTypeController.text, - ); - }, - ), - ); - if (datos != null) { - Professional? profesional = datos[0]; - ubicacion = datos[1]; - - if (profesional != null) { - if (mounted) { - setState(() { - _profesionalController.text = - profesional.name.toString(); - professionalId = profesional.id; - professionalTarifa = profesional.tarifa ?? 0; - professionalUbicacion = profesional.ubicacion; - professionalAddress = _ubicationController.text; - if (profesional.token != null && - profesional.token != '') { - professionalToken = profesional.token!; - } - - if (ubicacion == 'sitio') { - professionalAddress = profesional.realAddress; - _ubicationController.text = - profesional.realAddress; - latUser = profesional.latitude; - lngUser = profesional.longitude; - } - }); - } - } - } - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.assignment_ind_rounded), - suffixIcon: Icon(Icons.arrow_drop_down), - hintText: 'Seleccionar profesional'), - ), - const SizedBox(height: 20.0), - TextFormField( - controller: _ubicationController, - readOnly: true, - onTap: () async { - final List datos = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const UbicacionScreen(); - }, - ), - ); - - if (datos.length == 3) { - final formattedAddress = datos[0]; - final lat = datos[1]; - final lng = datos[2]; - - if (mounted) { - setState(() { - _ubicationController.text = formattedAddress; - latUser = lat; - lngUser = lng; - }); - } - } - }, - decoration: const InputDecoration( - hintText: 'Escribe tu ubicación', - prefixIcon: Icon(Icons.location_on), - ), - ), - const SizedBox(height: 20.0), - TextFormField( - onTap: () { - if (_profesionalController.text.isNotEmpty) { - _selectDate(context); - } else { - WarningSnackbar.show( - title: 'Selecciona un profesional', - message: - 'Selecciona un profesional antes de elegir la fecha y la hora de la cita.', - ); - } - }, - readOnly: true, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.calendar_month), - suffixIcon: _selectedDate == null - ? const Icon(Icons.arrow_drop_down) - : null, - hintText: 'Fecha', - ), - controller: TextEditingController( - text: _selectedDate == null - ? '' - : formatter.format(_selectedDate!)), - ), - const SizedBox(height: 20.0), - TextFormField( - onTap: () { - if (_profesionalController.text.isNotEmpty) { - _selectTime(context); - } else { - WarningSnackbar.show( - title: 'Selecciona un profesional', - message: - 'Selecciona un profesional antes de elegir la fecha y la hora de la cita.', - ); - } - }, - readOnly: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.access_time), - suffixIcon: Icon(Icons.arrow_drop_down), - hintText: 'Hora', - ), - controller: TextEditingController( - text: _selectedTime == null - ? '' - : ' ${_selectedTime!.format(context)}'), - ), - const SizedBox(height: 20.0), - TextFormField( - controller: _observacionController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.message_outlined), - hintText: 'Observaciones'), - ), - const SizedBox(height: 40.0), - ElevatedButton( - onPressed: () { - if (user?.name == null || - user?.name == '' || - user?.phoneNumber == null || - user?.phoneNumber == '') { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfileScreen(); - }, - ), - ); - } else { - try { - final DateTime combinedDate = DateTime( - _selectedDate!.year, - _selectedDate!.month, - _selectedDate!.day, - _selectedTime!.hour, - _selectedTime!.minute, - ); - - DateTime time2 = - combinedDate.add(const Duration(hours: 2)); - - eventoService - .createEvent( - user!.name, - _observacionController.text, - '$_selectedDate', - '$combinedDate', - '$time2', - professionalId, - ubicacion, - professionalAddress, - latUser, - lngUser, - 'pendiente', - professionalTarifa == 0 ? 0 : professionalTarifa, - false, - false, - ) - .then((value) { - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ServiceAfterScreen(eventoId: value); - }, - ), - ); - }); - } catch (e) { - WarningSnackbar.show( - title: 'Llena todos los campos', - message: - 'Asegurate de llenar todos los campos.', - ); - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 50), - ), - child: const Text( - 'Solicitar servicio', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/solicitudes.dart b/lib/src/presentation/screens/solicitudes.dart deleted file mode 100644 index 421189b..0000000 --- a/lib/src/presentation/screens/solicitudes.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:intl/intl.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/drawer_professional.dart'; -import 'package:prosappco/src/models/event_model.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/presentation/screens/cita.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class SolicitudScreen extends StatefulWidget { - SolicitudScreen({super.key}); - - @override - State createState() => _SolicitudScreenState(); -} - -class _SolicitudScreenState extends State { - List eventos = []; - bool loading = true; - final myUid = AuthenticationRepository.instance.getCurrentUserUid(); - - @override - void initState() { - super.initState(); - _load(); - } - - Future _load() async { - try { - final List data = await ApiService.instance - .get('/services/professional/requests'); - final List loaded = []; - for (final e in data) { - final event = Event.fromJson(e as Map); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - loaded.add(event); - } - loaded.sort((a, b) { - if (a.timeStamp == null || b.timeStamp == null) return 0; - return a.timeStamp!.compareTo(b.timeStamp!); - }); - if (mounted) setState(() { - eventos = loaded; - loading = false; - }); - } catch (e) { - print('Error loading solicitudes: $e'); - if (mounted) setState(() => loading = false); - } - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: AppBar( - backgroundColor: Colors.white, - iconTheme: const IconThemeData(color: Colors.black), - title: const Text( - 'Solicitudes', - style: TextStyle(color: Colors.black), - ), - ), - drawer: DrawerProfessional(), - body: loading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - child: Column(children: [_eventList(context)]), - ), - ), - ); - } - - Widget _eventList(BuildContext context) { - if (eventos.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Center(child: Text('No tienes citas')), - ); - } - return Column( - children: [ - ...eventos.map( - (event) => ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: event); - }, - ), - ); - }, - leading: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(DateFormat('h:mm a') - .format(DateTime.parse(event.range1Hour1))), - ], - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - event.title, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', - style: const TextStyle(color: Colors.grey, fontSize: 16), - ) - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: event.scoresModel?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '"${event.description}"', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - trailing: const Icon(Icons.keyboard_arrow_right), - ), - ), - ], - ); - } -} diff --git a/lib/src/presentation/screens/support.dart b/lib/src/presentation/screens/support.dart deleted file mode 100644 index 777ab63..0000000 --- a/lib/src/presentation/screens/support.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:community_material_icon/community_material_icon.dart'; -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:prosappco/src/models/setting_model.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class SupportScreen extends StatefulWidget { - const SupportScreen({super.key}); - - @override - State createState() => SsupportStateScreen(); -} - -class SsupportStateScreen extends State { - SettingModel? settings; - final String subject = 'Soporte Prosapp'; - final String body = ''; - - Future _sendWhatsapp(String? number) async { - final _whatsappUrl = 'https://api.whatsapp.com/send?phone=$number&text=Hola%21+soy+usuario+de+Prosapp+y+quisiera+conocer+mas+sobre+esta+app+%F0%9F%98%81'; - if (!await launch(_whatsappUrl)) { - throw Exception('Could not launch $_whatsappUrl'); - } - } - - Future _sendEmail(String recipients) async { - final uri = 'mailto:$recipients?subject=${Uri.encodeComponent(subject)}&body=${Uri.encodeComponent(body)}'; - if (await canLaunch(uri)) await launch(uri); - } - - @override - void initState() { - super.initState(); - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - }), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Soporte'), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 30, left: 35, right: 35), - child: Text( - '${settings?.titulo}', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 35), - child: Text('${settings?.parrafo}'), - ), - Row( - children: [ - const Expanded(child: SizedBox()), - ElevatedButton( - onPressed: () { - _sendWhatsapp(settings?.numero); - }, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - side: const BorderSide( - color: Color(0xFF2BA4EC), - width: 2, - ), - ), - ), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 18, horizontal: 0), - child: Icon( - CommunityMaterialIcons.whatsapp, - size: 30, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 20), - ElevatedButton( - onPressed: () => _sendEmail(settings?.email ?? ''), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - side: const BorderSide( - color: Color(0xFF2BA4EC), - width: 2, - ), - ), - ), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 18, horizontal: 0), - child: Icon( - Icons.email_outlined, - size: 30, - color: Colors.white, - ), - ), - ), - const Expanded(child: SizedBox()), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 50), - child: Column( - children: [ - const Text( - 'Horario de atención:', - style: TextStyle(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 20), - Text('${settings?.dias}'), - const SizedBox(height: 5), - Text('${settings?.horas}'), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/screens/ubicacion.dart b/lib/src/presentation/screens/ubicacion.dart deleted file mode 100644 index 29191f1..0000000 --- a/lib/src/presentation/screens/ubicacion.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; -import 'dart:convert'; -import 'package:prosappco/src/components/network_utility.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; - -class UbicacionScreen extends StatefulWidget { - const UbicacionScreen({super.key}); - - @override - State createState() => _UbicacionScreenState(); -} - -class _UbicacionScreenState extends State { - List _placesList = []; - String selectedPlace = ''; - String _coordsOfCity = '0.0,0.0'; - - @override - void initState() { - super.initState(); - - // ponytail: city coords default to 0.0,0.0; geocoding removed in migration - } - - void placeAutoComplete(String query) async { - Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", { - "input": query, - "location": _coordsOfCity, - }); - - String? response = await NetworkUtility.fetchUrl(uri); - - if (response != null) { - setState(() { - _placesList = jsonDecode(response.toString())['results']; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Tu ubicación'), - body: Center( - child: SizedBox( - width: 300, - child: Column( - children: [ - const SizedBox(height: 20.0), - TextFormField( - decoration: const InputDecoration( - hintText: 'Escribe tu ubicación', - prefixIcon: Icon(Icons.location_on), - ), - onChanged: (value) { - String modifiedValue = value.replaceAll(' ', '_'); - placeAutoComplete(modifiedValue); - }, - ), - const SizedBox(height: 20.0), - Expanded( - child: ListView.builder( - itemCount: _placesList.length, - itemBuilder: (context, index) { - return ListTile( - onTap: () { - Navigator.pop(context, [ - _placesList[index]['formatted_address'], - _placesList[index]['geometry']['location']['lat'], - _placesList[index]['geometry']['location']['lng'] - ]); - }, - title: Text(_placesList[index]['formatted_address']), - ); - }, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/presentation/screens/web_view.dart b/lib/src/presentation/screens/web_view.dart deleted file mode 100644 index d0cd6de..0000000 --- a/lib/src/presentation/screens/web_view.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -class WebViewScreen extends StatefulWidget { - String label; - String link; - WebViewScreen({super.key, required this.label, required this.link}); - - @override - State createState() => _WebViewScreenState(); -} - -class _WebViewScreenState extends State { - bool isLoading = true; - - late WebViewController controller; - - @override - void initState() { - super.initState(); - - controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(const Color(0x00000000)) - ..setNavigationDelegate( - NavigationDelegate( - onProgress: (int progress) {}, - onPageStarted: (String url) { - setState(() { - isLoading = true; - }); - }, - onPageFinished: (String url) { - setState(() { - isLoading = false; - }); - }, - onWebResourceError: (WebResourceError error) {}, - onNavigationRequest: (NavigationRequest request) { - if (request.url.startsWith('https://www.google.com/')) { - return NavigationDecision.prevent; - } - return NavigationDecision.navigate; - }, - ), - ) - ..loadRequest(Uri.parse(widget.link)); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: widget.label, - ), - body: Stack( - children: [ - if (isLoading) - const Center(child: CircularProgressIndicator()) - else - const SizedBox(), - WebViewWidget(controller: controller), - ], - ), - ); - } -} diff --git a/lib/src/presentation/widgets/profile/birth_date_picker.dart b/lib/src/presentation/widgets/profile/birth_date_picker.dart deleted file mode 100644 index 4954233..0000000 --- a/lib/src/presentation/widgets/profile/birth_date_picker.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; - -class BirthDatePicker extends StatefulWidget { - final Function(DateTime) onDateSelected; - final TextEditingController controller; - - const BirthDatePicker( - {Key? key, required this.onDateSelected, required this.controller}) - : super(key: key); - - @override - _BirthDatePickerState createState() => _BirthDatePickerState(); -} - -class _BirthDatePickerState extends State { - DateTime selectedDate = - DateTime.now().subtract(const Duration(days: 365 * 20)); - - Future _selectDate(BuildContext context) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: selectedDate, - firstDate: DateTime.now().subtract(const Duration(days: 365 * 120)), - lastDate: DateTime.now(), - ); - - if (picked != null && picked != selectedDate) { - setState(() { - selectedDate = picked; - widget.onDateSelected(selectedDate); - }); - } - } - - @override - Widget build(BuildContext context) { - return TextFormField( - onTap: () { - _selectDate(context); - }, - readOnly: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.calendar_month), - suffixIcon: Icon(Icons.arrow_drop_down), - hintText: 'Fecha de nacimiento', - ), - controller: widget.controller, - ); - } -} diff --git a/lib/src/presentation/widgets/profile/gender_dropdown.dart b/lib/src/presentation/widgets/profile/gender_dropdown.dart deleted file mode 100644 index 7a212c5..0000000 --- a/lib/src/presentation/widgets/profile/gender_dropdown.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; - -class GenderDropdown extends StatefulWidget { - final Function(String) onChanged; - - const GenderDropdown({Key? key, required this.onChanged}) : super(key: key); - - @override - _GenderDropdownState createState() => _GenderDropdownState(); -} - -class _GenderDropdownState extends State { - String? selectedGender; - - @override - Widget build(BuildContext context) { - return InputDecorator( - decoration: const InputDecoration( - border: UnderlineInputBorder(), - prefixIcon: Icon(Icons.people_outline), - contentPadding: EdgeInsets.symmetric(horizontal: 8.0), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: selectedGender, - hint: const Text('Selecciona tu género'), - onChanged: (String? newValue) { - setState(() { - selectedGender = newValue; - widget.onChanged(selectedGender!); - }); - }, - items: ['Masculino', 'Femenino', 'Otro'] - .map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ), - ); - } -} diff --git a/lib/src/presentation/widgets/shared/drawer_menu.dart b/lib/src/presentation/widgets/shared/drawer_menu.dart deleted file mode 100644 index 347ca84..0000000 --- a/lib/src/presentation/widgets/shared/drawer_menu.dart +++ /dev/null @@ -1,407 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; -import 'package:prosappco/src/components/photo_view.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; -import 'package:prosappco/src/providers/user_provider.dart'; -import 'package:prosappco/src/presentation/screens/configuracion.dart'; -import 'package:prosappco/src/presentation/screens/professional_profile_web.dart'; -import 'package:prosappco/src/presentation/screens/profile/profile.dart'; -import 'package:prosappco/src/presentation/screens/reputation.dart'; -import 'package:prosappco/src/presentation/screens/support.dart'; -import 'package:prosappco/src/presentation/screens/web_view.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:provider/provider.dart'; - -class DrawerMenu extends StatelessWidget { - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - - Future _irSugerencias() async { - const url = 'https://admin.prosapp.co/sugerencias'; - - final Uri _url = Uri.parse(url); - - if (await canLaunchUrl(_url)) { - await launchUrl(_url); - } else { - throw 'No se pudo abrir la URL $url'; - } - } - - @override - Widget build(BuildContext context) { - final userProvider = Provider.of(context); - UserModel? user = userProvider.user; - ScoresModel? score = userProvider.score; - - return Drawer( - child: Column( - children: [ - Container( - color: Colors.white, - child: Column( - children: [ - infoUser(context, user), - ], // - ), - ), - Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 0), // changes position of shadow - ), - ], - ), - child: Divider( - height: 0, - color: Colors.grey[300], - ), - ), - Expanded( - child: Column( - children: [ - ListTile( - onTap: () { - if (ModalRoute.of(context)?.settings.name != - '/misservicios') { - Navigator.pushNamed(context, '/misservicios'); - } else { - Scaffold.of(context).openEndDrawer(); - } - }, - leading: const Icon( - Icons.history, - color: Colors.black, - ), - title: const Text( - 'Mis servicios', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfileScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.person_outline, - color: Colors.black, - ), - title: const Text( - 'Mi perfil', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ConfiguracionScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.construction_outlined, - color: Colors.black, - ), - title: const Text( - 'Configuración', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const SupportScreen(); - }, - ), - ); - }, - leading: const Icon( - Icons.question_mark_rounded, - color: Colors.black, - ), - title: const Text( - 'Soporte', - style: TextStyle(fontSize: 15), - ), - ), - ListTile( - onTap: () { - if (kIsWeb) { - _irSugerencias(); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Sugerencias', - link: 'https://admin.prosapp.co/sugerencias', - ); - }, - ), - ); - } - }, - leading: const Icon( - Icons.campaign_outlined, - color: Colors.black, - ), - title: const Text( - 'Sugerencias', - style: TextStyle(fontSize: 15), - ), - ), - Builder(builder: (BuildContext context) { - return ListTile( - onTap: () { - if (ModalRoute.of(context)?.settings.name != - '/servicio') { - Navigator.pushNamed(context, '/servicio'); - } else { - Scaffold.of(context).openEndDrawer(); - } - }, - trailing: const Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - ), - title: const Text( - 'Solicitar servicio', - style: TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.bold), - ), - tileColor: const Color(0xFF2BA4EC), - contentPadding: - const EdgeInsets.symmetric(vertical: 5, horizontal: 16), - ); - }), - Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 3, - offset: - const Offset(0, 2), // changes position of shadow - ), - ], - ), - child: Container( - color: const Color(0xFFD6F4FF), - child: ListTile( - tileColor: const Color(0xFFD6F4FF), - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ReputationScreen(); - }, - ), - ); - }, - trailing: const Icon(Icons.keyboard_arrow_right, - color: Colors.black), - title: const Text( - 'Reputación', - style: TextStyle(color: Colors.black), - ), - subtitle: Row( - children: [ - RatingBar.builder( - initialRating: score?.average ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemPadding: - const EdgeInsets.symmetric(horizontal: 0), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), - ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '(${score?.total.toString()}) ${score?.average.toStringAsFixed(1)}'), - ], - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Prosapp', - style: TextStyle(fontSize: 10, color: Colors.grey[700]), - ), - Padding( - padding: const EdgeInsets.only(top: 7, left: 3, right: 3), - child: Text( - '®', - style: TextStyle(fontSize: 25, color: Colors.grey[700]), - ), - ), - Text( - 'todos los derechos reservados', - style: TextStyle(fontSize: 10, color: Colors.grey[700]), - ), - ], - ), - ], - ), - ), - Column( - children: [ - ElevatedButton( - onPressed: () { - UserModel? user = userProvider.user; - if (user?.name == '' || - user?.city == '' || - user?.phoneNumber == '' || - user?.phoneNumber == null) { - WarningSnackbar.show( - title: 'Completa tu perfil', - message: - 'Para ser un profesional registrado, asegúrate de llenar todos los campos necesarios y no olvides guardar tus cambios para que surtan efecto.', - ); - } else { - if (user?.phoneNumber != user?.phoneNumber) { - WarningSnackbar.show( - title: 'Tu número de teléfono sigue sin cambios.', - message: 'Para guardar esta información, dirígete a tu perfil.', - ); - } else { - if (user?.proState == 0 || user?.proState == null) { - Navigator.pop(context); - if (kIsWeb) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ProfessionalProfileWebScreen(), - ), - ); - } else { - Navigator.pushNamed(context, '/profesionalProfile'); - } - } else if (user?.proState == 1) { - Navigator.pushNamed(context, '/profesionalRevision'); - } else if (user?.proState == 2) { - Navigator.pushNamed(context, '/solicitud'); - } - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 45), - ), - child: const Text( - 'Modo profesional', - style: TextStyle( - color: Colors.white, - fontSize: 15, - ), - ), - ), - const SizedBox(height: 5), - ElevatedButton( - onPressed: () async { - await AuthenticationRepository.instance.logout(); - userProvider.setNullUser(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - minimumSize: const Size(230, 40), - ), - child: const Text( - 'Cerrar Sesión', - style: TextStyle( - color: Colors.white, - fontSize: 15, - ), - ), - ), - ], - ), - const SizedBox(height: 10), - ], - ), - ); - } - - ListTile infoUser(BuildContext context, UserModel? user) { - return ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfileScreen(); - }, - ), - ); - }, - title: Text(user?.name ?? '', - style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user?.phoneNumber ?? '', - style: const TextStyle(fontSize: 12), - ), - Text( - user?.city ?? '', - style: const TextStyle(fontSize: 12), - ), - ], - ), - leading: ReferencePhoto( - ref: user?.picture, - size: 50, - sizeCircle: 50, - ), - trailing: const Icon(Icons.keyboard_arrow_right, color: Colors.black), - contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), - ); - } -} diff --git a/lib/src/presentation/widgets/shared/loading_item_list.dart b/lib/src/presentation/widgets/shared/loading_item_list.dart deleted file mode 100644 index f4efedd..0000000 --- a/lib/src/presentation/widgets/shared/loading_item_list.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; - -class LoadingItemList extends StatelessWidget { - final bool useCircleAvatar; - - const LoadingItemList({super.key, required this.useCircleAvatar}); - - @override - Widget build(BuildContext context) { - return Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: ListTile( - leading: useCircleAvatar - ? const CircleAvatar( - radius: 25, - backgroundColor: Colors.white, - ) - : Container( - width: 45, - height: 20, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - ), - title: Container( - height: 20, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - ), - subtitle: Container( - height: 15, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - ), - trailing: const Icon( - Icons.chevron_right, - color: Colors.white, - size: 30, - ), - ), - ); - } -} diff --git a/lib/src/presentation/widgets/shared/notificacion_snackbar.dart b/lib/src/presentation/widgets/shared/notificacion_snackbar.dart deleted file mode 100644 index 546d401..0000000 --- a/lib/src/presentation/widgets/shared/notificacion_snackbar.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; - -class NotificationSnackbar { - static void show({ - required String title, - required String message, - Color backgroundColor = Colors.red, - Color textColor = Colors.white, - Icon icon = const Icon(Icons.warning, color: Colors.white), - }) { - showAppSnackBar(title, message, color: backgroundColor); - } -} diff --git a/lib/src/presentation/widgets/shared/primary_button.dart b/lib/src/presentation/widgets/shared/primary_button.dart deleted file mode 100644 index 2ecbea1..0000000 --- a/lib/src/presentation/widgets/shared/primary_button.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -class PrimaryButton extends StatelessWidget { - final VoidCallback onPressed; - final String text; - final double? minWidth; - final double? minHeight; - final bool isEnabled; - - const PrimaryButton({ - super.key, - required this.onPressed, - required this.text, - this.minWidth = 200, - this.minHeight = 50, - this.isEnabled = true, - }); - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: isEnabled ? onPressed : null, - style: ElevatedButton.styleFrom( - backgroundColor: isEnabled ? const Color(0xFF2BA4EC) : Colors.grey, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: Size(minWidth!, minHeight!), - ), - child: Text( - text, - style: TextStyle( - color: isEnabled ? Colors.white : Colors.black, - fontWeight: FontWeight.bold, - fontSize: 17, - ), - ), - ); - } -} diff --git a/lib/src/presentation/widgets/shared/primary_checkbox.dart b/lib/src/presentation/widgets/shared/primary_checkbox.dart deleted file mode 100644 index fa448d7..0000000 --- a/lib/src/presentation/widgets/shared/primary_checkbox.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -class PrimaryCheckbox extends StatelessWidget { - final String text; - final bool initialValue; - final Function(bool) onChanged; - - const PrimaryCheckbox({ - Key? key, - required this.text, - required this.initialValue, - required this.onChanged, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return CheckboxListTile( - title: Text( - text, - style: const TextStyle(fontSize: 15), - ), - value: initialValue, - onChanged: (value) { - onChanged(value!); - }, - ); - } -} diff --git a/lib/src/presentation/widgets/shared/warning_snackbar.dart b/lib/src/presentation/widgets/shared/warning_snackbar.dart deleted file mode 100644 index 728bb49..0000000 --- a/lib/src/presentation/widgets/shared/warning_snackbar.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/utils/app_navigator.dart'; - -class WarningSnackbar { - static void show({ - required String title, - required String message, - Color backgroundColor = Colors.red, - Color textColor = Colors.white, - Icon icon = const Icon(Icons.warning, color: Colors.white), - }) { - showAppSnackBar(title, message, color: backgroundColor); - } -} diff --git a/lib/src/providers/user_provider.dart b/lib/src/providers/user_provider.dart deleted file mode 100644 index bbd185f..0000000 --- a/lib/src/providers/user_provider.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:prosappco/src/models/scores_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; -import 'package:prosappco/src/services/api_service.dart'; - -class UserProvider extends ChangeNotifier { - final _api = ApiService.instance; - UserModel? _user; - ScoresModel? _score; - - UserProvider() { - loadUser(); - } - - Future loadUser() async { - final token = await _api.getToken(); - if (token == null) return; - try { - final data = await _api.get('/auth/me'); - _user = UserModel.fromApi(data as Map); - _score = await ScoresModel.scoreTo(_user?.id, false, false); - notifyListeners(); - } catch (_) {} - } - - Future initUserProvider() => loadUser(); - Future updateUserDataAndScores() => loadUser(); - - void setNullUser() { - _user = null; - _score = null; - notifyListeners(); - } - - UserModel? get user => _user; - ScoresModel? get score => _score; -} diff --git a/lib/src/services/api_service.dart b/lib/src/services/api_service.dart deleted file mode 100644 index 5b39536..0000000 --- a/lib/src/services/api_service.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:shared_preferences/shared_preferences.dart'; - -const String _baseUrl = 'https://backend.prosapp.co/api/v1'; - -class ApiException implements Exception { - final int statusCode; - final String message; - ApiException(this.statusCode, this.message); - @override - String toString() => 'ApiException($statusCode): $message'; -} - -class ApiService { - static ApiService? _instance; - static ApiService get instance => _instance ??= ApiService._(); - static const baseUrl = _baseUrl; - ApiService._(); - - Map parseJson(String body) => jsonDecode(body) as Map; - - String? _token; - - Future getToken() async { - if (_token != null) return _token; - final prefs = await SharedPreferences.getInstance(); - _token = prefs.getString('token'); - return _token; - } - - Future saveToken(String token) async { - _token = token; - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('token', token); - } - - Future clearToken() async { - _token = null; - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('token'); - } - - Future> _headers({bool auth = true}) async { - final headers = {'Content-Type': 'application/json'}; - if (auth) { - final token = await getToken(); - if (token != null) headers['Authorization'] = 'Bearer $token'; - } - return headers; - } - - dynamic _parse(http.Response res) { - final body = jsonDecode(res.body); - if (res.statusCode >= 200 && res.statusCode < 300) return body; - final msg = body is Map ? (body['message'] ?? res.reasonPhrase) : res.reasonPhrase; - throw ApiException(res.statusCode, msg.toString()); - } - - Future get(String path, {bool auth = true}) async { - final res = await http.get( - Uri.parse('$_baseUrl$path'), - headers: await _headers(auth: auth), - ); - return _parse(res); - } - - Future post(String path, Map body, {bool auth = false}) async { - final res = await http.post( - Uri.parse('$_baseUrl$path'), - headers: await _headers(auth: auth), - body: jsonEncode(body), - ); - return _parse(res); - } - - Future patch(String path, Map body, {bool auth = true}) async { - final res = await http.patch( - Uri.parse('$_baseUrl$path'), - headers: await _headers(auth: auth), - body: jsonEncode(body), - ); - return _parse(res); - } - - Future delete(String path, {bool auth = true}) async { - final res = await http.delete( - Uri.parse('$_baseUrl$path'), - headers: await _headers(auth: auth), - ); - return _parse(res); - } -} diff --git a/lib/src/services/firebase_messaging.dart b/lib/src/services/firebase_messaging.dart deleted file mode 100644 index adff9fc..0000000 --- a/lib/src/services/firebase_messaging.dart +++ /dev/null @@ -1,6 +0,0 @@ -// ponytail: FCM not applicable on web; stub so imports compile -class FirebaseMessagingService { - Future initializeFirebaseMessaging() async { - // No-op: push notifications handled server-side - } -} diff --git a/lib/src/services/local_notifications.dart b/lib/src/services/local_notifications.dart deleted file mode 100644 index 03b5bbb..0000000 --- a/lib/src/services/local_notifications.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; - -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - -Future initNotifications() async { - const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings('@mipmap/ic_launcher'); - - const DarwinInitializationSettings initializationSettingsIOS = - DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestSoundPermission: true, - ); - const InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS, - ); - - await flutterLocalNotificationsPlugin.initialize(initializationSettings); -} - -Future showNotification(String title, String body) async { - const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails('solicitud_servicio', 'Solicitud de Servicio', - importance: Importance.max, priority: Priority.high); - -// IOSNotificationDetails --> DarwinNotificationDetails - const DarwinNotificationDetails iOSNotificationDetails = - DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - ); - const NotificationDetails notificationDetails = NotificationDetails( - android: androidNotificationDetails, iOS: iOSNotificationDetails); - - await flutterLocalNotificationsPlugin.show( - 1, title, body, notificationDetails); -} diff --git a/lib/src/services/select_image_profile.dart b/lib/src/services/select_image_profile.dart deleted file mode 100644 index a972fb6..0000000 --- a/lib/src/services/select_image_profile.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:image_picker/image_picker.dart'; - -Future> getImage(opc) async { - final ImagePicker picker = ImagePicker(); - - if (opc == 1) { - XFile? image = await picker.pickImage(source: ImageSource.camera); - return [image]; - } else if (opc == 2) { - XFile? image = await picker.pickImage(source: ImageSource.gallery); - return [image]; - } else { - final List images = await picker.pickMultiImage(); - return images; - } -} diff --git a/lib/src/utils/app_navigator.dart b/lib/src/utils/app_navigator.dart deleted file mode 100644 index 812be99..0000000 --- a/lib/src/utils/app_navigator.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -final appNavigatorKey = GlobalKey(); - -void pushOffAll(Widget screen) { - appNavigatorKey.currentState?.pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => screen), - (_) => false, - ); -} - -void push(Widget screen) { - appNavigatorKey.currentState?.push( - MaterialPageRoute(builder: (_) => screen), - ); -} - -void back() => appNavigatorKey.currentState?.pop(); - -void showAppSnackBar(String title, String message, {Color color = Colors.red}) { - final ctx = appNavigatorKey.currentContext; - if (ctx == null) return; - ScaffoldMessenger.of(ctx).showSnackBar(SnackBar( - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), - Text(message, style: TextStyle(color: Colors.white)), - ], - ), - backgroundColor: color, - duration: const Duration(seconds: 4), - behavior: SnackBarBehavior.floating, - )); -} - -Future showAppDialog(AlertDialog dialog) { - final ctx = appNavigatorKey.currentContext!; - return showDialog(context: ctx, builder: (_) => dialog); -} diff --git a/lib/ui/buttons/custom_outlined_button.dart b/lib/ui/buttons/custom_outlined_button.dart new file mode 100644 index 0000000..68f4b24 --- /dev/null +++ b/lib/ui/buttons/custom_outlined_button.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class CustomOutlinedButton extends StatelessWidget { + final Function onPressed; + final String text; + final Color color; + final bool isFilled; + + const CustomOutlinedButton({ + super.key, + required this.onPressed, + required this.text, + this.color = Colors.blue, + this.isFilled = false, + }); + + @override + Widget build(BuildContext context) { + return OutlinedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + side: WidgetStateProperty.all( + BorderSide(color: color), + ), + backgroundColor: WidgetStateProperty.all( + isFilled ? color.withOpacity(0.3) : Colors.transparent, + ), + ), + onPressed: () => onPressed(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Text( + text, + style: const TextStyle( + fontSize: 16, + color: Colors.blue, + ), + ), + ), + ); + } +} diff --git a/lib/ui/buttons/link_text.dart b/lib/ui/buttons/link_text.dart new file mode 100644 index 0000000..07fb85e --- /dev/null +++ b/lib/ui/buttons/link_text.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class LinkText extends StatefulWidget { + final String text; + final Function()? onPressed; + + const LinkText({super.key, required this.text, this.onPressed}); + + @override + State createState() => _LinkTextState(); +} + +class _LinkTextState extends State { + bool isHover = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (widget.onPressed != null) widget.onPressed!(); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => isHover = true), + onExit: (_) => setState(() => isHover = false), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + child: Text( + widget.text, + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], + decoration: isHover ? TextDecoration.underline : null, + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/cards/white_card.dart b/lib/ui/cards/white_card.dart new file mode 100644 index 0000000..ec79aae --- /dev/null +++ b/lib/ui/cards/white_card.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class WhiteCard extends StatelessWidget { + final String? title; + final Widget child; + final double? width; + + const WhiteCard({super.key, this.title, required this.child, this.width}); + + @override + Widget build(BuildContext context) { + return Container( + width: width, // width == null ? null : width, + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(10), + decoration: buildBoxDecoration(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) ...[ + FittedBox( + fit: BoxFit.contain, + child: Text( + title!, + style: GoogleFonts.roboto( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + const Divider() + ], + child + ], + ), + ); + } + + BoxDecoration buildBoxDecoration() { + return BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5) + ], + ); + } +} diff --git a/lib/ui/inputs/custom_inputs.dart b/lib/ui/inputs/custom_inputs.dart new file mode 100644 index 0000000..925a63a --- /dev/null +++ b/lib/ui/inputs/custom_inputs.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class CustomInputs { + static InputDecoration loginInputDecoration({ + required String hint, + required String label, + required IconData icon, + }) { + return InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black.withOpacity(0.3)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black.withOpacity(0.3)), + ), + hintText: hint, + labelText: label, + prefixIcon: Icon(icon, color: Colors.grey), + labelStyle: const TextStyle(color: Colors.grey), + hintStyle: const TextStyle(color: Colors.grey), + ); + } + + static InputDecoration searchInputDecoration({ + required String hint, + required IconData icon, + }) { + return InputDecoration( + enabledBorder: InputBorder.none, + border: InputBorder.none, + hintText: hint, + prefixIcon: Icon(icon, color: Colors.grey), + labelStyle: const TextStyle(color: Colors.grey), + hintStyle: const TextStyle(color: Colors.grey), + ); + } + + static InputDecoration formInputDecoration({ + required String hint, + required String label, + required IconData icon, + IconButton? iconButton, + }) { + return InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.indigo.withOpacity(0.3)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.indigo.withOpacity(0.3)), + ), + hintText: hint, + labelText: label, + prefixIcon: Icon(icon, color: Colors.grey), + suffixIcon: iconButton, + labelStyle: const TextStyle(color: Colors.grey), + hintStyle: const TextStyle(color: Colors.grey), + ); + } +} diff --git a/lib/ui/labels/custom_labels.dart b/lib/ui/labels/custom_labels.dart new file mode 100644 index 0000000..b66cb5c --- /dev/null +++ b/lib/ui/labels/custom_labels.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class CustomLabels { + static TextStyle h1 = GoogleFonts.roboto( + fontSize: 30, + fontWeight: FontWeight.w400, + ); + static TextStyle h2 = GoogleFonts.roboto( + fontSize: 20, + fontWeight: FontWeight.w400, + ); + static TextStyle h3 = GoogleFonts.roboto( + fontSize: 18, + fontWeight: FontWeight.w400, + ); + static TextStyle h4 = GoogleFonts.roboto( + fontSize: 13, + fontWeight: FontWeight.w400, + ); + static TextStyle h5 = GoogleFonts.roboto( + fontSize: 15, + fontWeight: FontWeight.w400, + fontStyle: FontStyle.italic, + ); +} diff --git a/lib/ui/layouts/auth/auth_layout.dart b/lib/ui/layouts/auth/auth_layout.dart new file mode 100644 index 0000000..c0a4b45 --- /dev/null +++ b/lib/ui/layouts/auth/auth_layout.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:prosapp_web_app/ui/layouts/auth/widgets/custom_title.dart'; +import 'package:prosapp_web_app/ui/layouts/auth/widgets/background_auth.dart'; + +class AuthLayout extends StatelessWidget { + final Widget child; + const AuthLayout({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Scaffold( + body: Scrollbar( + child: ListView( + physics: const ClampingScrollPhysics(), + children: [ + size.width > 1000 + ? _DesktopBody(child: child) + : _MobileBody(child: child), + ], + ), + ), + ); + } +} + +class _MobileBody extends StatelessWidget { + final Widget child; + + const _MobileBody({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + const CustomTitle(), + SizedBox(width: double.infinity, height: 420, child: child), + const SizedBox( + width: double.infinity, + height: 400, + child: BackgroundAuth(), + ) + ], + ), + ); + } +} + +class _DesktopBody extends StatelessWidget { + final Widget child; + + const _DesktopBody({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Container( + width: size.width, + height: size.height, + color: Colors.white, + child: Row( + children: [ + const Expanded(child: BackgroundAuth()), + + // View container + Container( + width: 600, + height: double.infinity, + color: Colors.white, + child: Column( + children: [ + const SizedBox(height: 20), + const CustomTitle(), + const SizedBox(height: 50), + Expanded(child: child), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/layouts/auth/widgets/background_auth.dart b/lib/ui/layouts/auth/widgets/background_auth.dart new file mode 100644 index 0000000..f16c95e --- /dev/null +++ b/lib/ui/layouts/auth/widgets/background_auth.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class BackgroundAuth extends StatelessWidget { + const BackgroundAuth({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.blue, + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + child: const Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 30), + child: Image( + image: AssetImage('prosapp-logo.png'), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/layouts/auth/widgets/custom_title.dart b/lib/ui/layouts/auth/widgets/custom_title.dart new file mode 100644 index 0000000..2f74022 --- /dev/null +++ b/lib/ui/layouts/auth/widgets/custom_title.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class CustomTitle extends StatelessWidget { + const CustomTitle({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 100), // Añade espacio arriba + Image.asset( + 'logo.png', + width: 60, + height: 60, + ), + const SizedBox(height: 40), // Mantiene el espacio entre el logo y el texto + FittedBox( + fit: BoxFit.contain, + child: Text( + "Iniciar sesión", + style: GoogleFonts.montserratAlternates( + fontSize: 30, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/ui/layouts/dashboard/dashboard_layout.dart b/lib/ui/layouts/dashboard/dashboard_layout.dart new file mode 100644 index 0000000..4a228b5 --- /dev/null +++ b/lib/ui/layouts/dashboard/dashboard_layout.dart @@ -0,0 +1,85 @@ +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/ui/shared/navbar.dart'; +import 'package:prosapp_web_app/ui/shared/sidebar.dart'; +import 'package:flutter/material.dart'; + +class DashboardLayout extends StatefulWidget { + final Widget child; + const DashboardLayout({super.key, required this.child}); + + @override + State createState() => _DashboardLayoutState(); +} + +class _DashboardLayoutState extends State + with SingleTickerProviderStateMixin { + @override + void initState() { + super.initState(); + SideMenuProvider.menuController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Scaffold( + backgroundColor: const Color(0xffEDF1F2), + body: Stack( + children: [ + Row( + children: [ + if (size.width >= 700) const Sidebar(), + Expanded( + child: Column( + children: [ + // Navbar + const Navbar(), + + // View + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + child: widget.child, + ), + ), + ], + ), + ) + ], + ), + if (size.width < 700) + AnimatedBuilder( + animation: SideMenuProvider.menuController, + builder: (context, _) => Stack( + children: [ + if (SideMenuProvider.isOpen) + Opacity( + opacity: SideMenuProvider.opacity.value, + child: GestureDetector( + onTap: () => SideMenuProvider.closeMenu(), + child: Container( + width: size.width, + height: size.height, + color: Colors.black26, + ), + ), + ), + Transform.translate( + offset: Offset(SideMenuProvider.movement.value, 0), + child: const Sidebar(), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/layouts/splash/splash_layout.dart b/lib/ui/layouts/splash/splash_layout.dart new file mode 100644 index 0000000..4936645 --- /dev/null +++ b/lib/ui/layouts/splash/splash_layout.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class SplashLayout extends StatelessWidget { + const SplashLayout({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text('Checking...'), + ], + ), + ), + ); + } +} diff --git a/lib/ui/shared/navbar.dart b/lib/ui/shared/navbar.dart new file mode 100644 index 0000000..e9389f5 --- /dev/null +++ b/lib/ui/shared/navbar.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/score_provider.dart'; + +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; + +import 'package:prosapp_web_app/ui/shared/widgets/navbar_avatar.dart'; +import 'package:provider/provider.dart'; + +class Navbar extends StatelessWidget { + const Navbar({super.key}); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + final professionalProvider = Provider.of(context); + final scoreProvider = Provider.of(context, listen: false); + + WidgetsBinding.instance.addPostFrameCallback((_) { + scoreProvider.loadReputation(context.read().user!.id); + }); + + return Consumer(builder: (context, scoreProvider, child) { + final reputation = scoreProvider.reputation; + + return Container( + width: double.infinity, + height: 50, + decoration: buildBoxDecoration(), + child: Row( + children: [ + if (size.width <= 700) ...[ + IconButton( + icon: const Icon(Icons.menu_outlined), + onPressed: () => SideMenuProvider.openMenu(), + ), + ], + const SizedBox(width: 5), + const Spacer(), + Row( + children: [ + Icon(Icons.star, color: Colors.yellow[600], size: 15), + const SizedBox(width: 2), + Text( + professionalProvider.isProModeActive + ? reputation.averagePro.toStringAsFixed(2) + : reputation.average.toStringAsFixed(2), + style: const TextStyle(fontSize: 15), + ), + ], + ), + const SizedBox(width: 10), + const NavbarAvatar(), + const SizedBox(width: 10), + ], + ), + ); + }); + } + + BoxDecoration buildBoxDecoration() { + return const BoxDecoration(color: Colors.white, boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 5, + ), + ]); + } +} diff --git a/lib/ui/shared/sidebar.dart b/lib/ui/shared/sidebar.dart new file mode 100644 index 0000000..bad6fe8 --- /dev/null +++ b/lib/ui/shared/sidebar.dart @@ -0,0 +1,214 @@ +import 'package:prosapp_web_app/models/pro_state.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/shared/widgets/menu_item.dart'; +import 'package:prosapp_web_app/ui/shared/widgets/text_separator.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'package:prosapp_web_app/ui/shared/widgets/logo.dart'; +import 'package:provider/provider.dart'; + +class Sidebar extends StatelessWidget { + const Sidebar({super.key}); + + void navigateTo(String routeName) { + NavigationService.replaceTo(routeName); + SideMenuProvider.closeMenu(); + } + + @override + Widget build(BuildContext context) { + final sideMenuProvider = Provider.of(context); + final authProvider = Provider.of(context, listen: false); + final professionalProvider = Provider.of(context); + + return Container( + width: 200, + height: double.infinity, + decoration: buildBoxDecoration(), + child: ListView( + physics: const ClampingScrollPhysics(), + children: [ + const Logo(), + const SizedBox(height: 50), + if (professionalProvider.isProModeActive) ...[ + const TextSeparator(text: 'Profesional'), + MenuItem( + isActive: + sideMenuProvider.currentPage == Flurorouter.profileRoute || + sideMenuProvider.currentPage == + Flurorouter.professionalProfileRoute, + text: 'Perfil', + icon: Icons.person_outlined, + onPressed: () { + if (professionalProvider.isProModeActive) { + navigateTo(Flurorouter.professionalProfileRoute); + } else { + navigateTo(Flurorouter.profileRoute); + } + } + + // () => navigateTo(Flurorouter.profileRoute), + ), + MenuItem( + isActive: sideMenuProvider.currentPage == + Flurorouter.professionalServicesRequestsRoute, + text: 'Mis solicitudes', + icon: Icons.room_service_outlined, + onPressed: () => { + Provider.of(context, listen: false) + .clearServices(), + navigateTo(Flurorouter.professionalServicesRequestsRoute) + }, + ), + MenuItem( + isActive: sideMenuProvider.currentPage == + Flurorouter.professionalServicesRoute, + text: 'Mis servicios', + icon: Icons.list, + onPressed: () => { + Provider.of(context, listen: false) + .clearServices(), + navigateTo(Flurorouter.professionalServicesRoute) + }, + ), + MenuItem( + isActive: sideMenuProvider.currentPage == + Flurorouter.professionalServicesHistoryRoute, + text: 'Historial', + icon: Icons.restore_outlined, + onPressed: () => { + Provider.of(context, listen: false) + .clearServices(), + navigateTo(Flurorouter.professionalServicesHistoryRoute) + }, + ), + MenuItem( + text: 'Calendario', + icon: Icons.calendar_month_outlined, + onPressed: () => + navigateTo(Flurorouter.professionalCalendarRoute), + ), + ] else ...[ + const TextSeparator(text: 'Usuario'), + MenuItem( + isActive: + sideMenuProvider.currentPage == Flurorouter.profileRoute, + text: 'Perfil', + icon: Icons.person_outlined, + onPressed: () => navigateTo(Flurorouter.profileRoute), + ), + MenuItem( + isActive: + sideMenuProvider.currentPage == Flurorouter.dashboardRoute, + text: 'Solicitar', + icon: Icons.room_service_outlined, + onPressed: () => navigateTo(Flurorouter.dashboardRoute), + ), + MenuItem( + isActive: + sideMenuProvider.currentPage == Flurorouter.userServicesRoute, + text: 'Mis servicios', + icon: Icons.list, + onPressed: () => { + Provider.of(context, listen: false) + .clearServices(), + navigateTo(Flurorouter.userServicesRoute) + }, + ), + MenuItem( + isActive: sideMenuProvider.currentPage == + Flurorouter.userServicesHistoryRoute, + text: 'Historial', + icon: Icons.restore_outlined, + onPressed: () => { + Provider.of(context, listen: false) + .clearServices(), + navigateTo(Flurorouter.userServicesHistoryRoute) + }, + ), + MenuItem( + isActive: + sideMenuProvider.currentPage == Flurorouter.supportRoute, + text: 'Soporte', + icon: Icons.support_agent_outlined, + onPressed: () => navigateTo(Flurorouter.supportRoute), + ), + ], + Material( + color: Colors.blueAccent, + child: InkWell( + onTap: () { + if (authProvider.user?.picture == '' || + authProvider.user?.city == '' || + authProvider.user?.name == null || + authProvider.user?.name == '' || + authProvider.user?.email == '') { + NotificationsService.showSnackBarError( + 'Primero completa tu perfil'); + navigateTo(Flurorouter.profileRoute); + + return; + } + + if (authProvider.user?.proState == ProState.active) { + professionalProvider.toggleProMode(); + + if (professionalProvider.isProModeActive) { + navigateTo(Flurorouter.professionalServicesRequestsRoute); + } else { + navigateTo(Flurorouter.dashboardRoute); + } + } else { + navigateTo(Flurorouter.requestProfessionalRoute); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + vertical: 10, + ), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Text( + 'Modo ${professionalProvider.isProModeActive ? 'Usuario' : 'Profesional'}', + style: GoogleFonts.roboto( + color: Colors.white.withOpacity(0.8)), + ), + ), + ), + ), + ), + const SizedBox(height: 50), + const TextSeparator(text: 'Salir'), + MenuItem( + text: 'Cerrar sesión', + icon: Icons.exit_to_app_outlined, + onPressed: () => authProvider.logout(), + ), + ], + ), + ); + } + + BoxDecoration buildBoxDecoration() => const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xff092044), + Color(0xff092042), + ], + ), + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 10, + ), + ], + ); +} diff --git a/lib/ui/shared/widgets/logo.dart b/lib/ui/shared/widgets/logo.dart new file mode 100644 index 0000000..9601d26 --- /dev/null +++ b/lib/ui/shared/widgets/logo.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class Logo extends StatelessWidget { + const Logo({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(top: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('logo.png', width: 50, height: 50), + SizedBox(width: 8), + Text( + 'Prosapp', + style: GoogleFonts.montserratAlternates( + fontSize: 20, + fontWeight: FontWeight.w200, + color: Colors.white, + ), + ), + SizedBox(width: 10), + ], + ), + ); + } +} diff --git a/lib/ui/shared/widgets/menu_item.dart b/lib/ui/shared/widgets/menu_item.dart new file mode 100644 index 0000000..9baaaba --- /dev/null +++ b/lib/ui/shared/widgets/menu_item.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class MenuItem extends StatefulWidget { + final String text; + final IconData icon; + final bool isActive; + final Function onPressed; + + const MenuItem({ + super.key, + required this.text, + required this.icon, + this.isActive = false, + required this.onPressed, + }); + + @override + State createState() => _MenuItemState(); +} + +class _MenuItemState extends State { + bool isHovered = false; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + color: isHovered + ? Colors.white.withOpacity(0.1) + : widget.isActive + ? Colors.white.withOpacity(0.1) + : Colors.transparent, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.isActive ? null : () => widget.onPressed(), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + vertical: 10, + ), + child: MouseRegion( + cursor: widget.isActive + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + widget.icon, + color: Colors.white.withOpacity(0.3), + ), + const SizedBox(width: 10), + Text( + widget.text, + style: GoogleFonts.roboto( + color: Colors.white.withOpacity(0.8), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/shared/widgets/navbar_avatar.dart b/lib/ui/shared/widgets/navbar_avatar.dart new file mode 100644 index 0000000..9e2a6dd --- /dev/null +++ b/lib/ui/shared/widgets/navbar_avatar.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:provider/provider.dart'; + +class NavbarAvatar extends StatelessWidget { + const NavbarAvatar({super.key}); + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).user!; + + final image = (user.picture == '' || user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: user.picture!, + ); + + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => NavigationService.replaceTo(Flurorouter.profileRoute), + child: SizedBox( + width: 40, + height: 40, + child: ClipOval(child: image), + ), + ), + ); + } +} diff --git a/lib/ui/shared/widgets/schedule_day_tile.dart b/lib/ui/shared/widgets/schedule_day_tile.dart new file mode 100644 index 0000000..2749908 --- /dev/null +++ b/lib/ui/shared/widgets/schedule_day_tile.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; + +class ScheduleDayTile extends StatelessWidget { + final String day; + final ScheduleEntity schedule; + final Function(bool) onEnableChanged; + final Function(bool) onContinuousDayChanged; + final Function(TimeOfDay) onRange1Hour1Pick; + final Function(TimeOfDay) onRange1Hour2Pick; + final Function(TimeOfDay) onRange2Hour1Pick; + final Function(TimeOfDay) onRange2Hour2Pick; + + const ScheduleDayTile({ + super.key, + required this.day, + required this.schedule, + required this.onEnableChanged, + required this.onContinuousDayChanged, + required this.onRange1Hour1Pick, + required this.onRange1Hour2Pick, + required this.onRange2Hour1Pick, + required this.onRange2Hour2Pick, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SwitchListTile( + title: Text(day), + value: schedule.enabled, + onChanged: onEnableChanged, + ), + if (schedule.enabled) ...[ + SwitchListTile( + title: const Text('Jornada continua'), + value: schedule.continuousDay, + onChanged: onContinuousDayChanged, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + timePickerField( + context: context, + text: ScheduleEntity.getFormatTime(schedule.range1Hour1), + onPick: onRange1Hour1Pick, + ), + Center( + child: Container( + width: 10, + height: 1, + color: Colors.black, + ), + ), + if (!schedule.continuousDay) ...[ + timePickerField( + context: context, + text: ScheduleEntity.getFormatTime(schedule.range1Hour2), + onPick: onRange1Hour2Pick, + ), + const SizedBox(width: 10), + timePickerField( + context: context, + text: ScheduleEntity.getFormatTime(schedule.range2Hour1), + onPick: onRange2Hour1Pick, + ), + Center( + child: Container( + width: 10, + height: 1, + color: Colors.black, + ), + ), + ], + timePickerField( + context: context, + text: ScheduleEntity.getFormatTime(schedule.range2Hour2), + onPick: onRange2Hour2Pick, + ), + ], + ), + ], + ], + ); + } + + Widget timePickerField({ + required BuildContext context, + required String? text, + required Function(TimeOfDay) onPick, + }) { + return Expanded( + child: TextField( + readOnly: true, + controller: TextEditingController( + text: text, + ), + textAlign: TextAlign.center, + onTap: () async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (pickedTime != null) { + onPick.call(pickedTime); + } + }, + decoration: const InputDecoration( + hintText: 'Hora', + border: UnderlineInputBorder(), + ), + ), + ); + } +} diff --git a/lib/ui/shared/widgets/search_text.dart b/lib/ui/shared/widgets/search_text.dart new file mode 100644 index 0000000..59b8771 --- /dev/null +++ b/lib/ui/shared/widgets/search_text.dart @@ -0,0 +1,25 @@ +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:flutter/material.dart'; + +class SearchText extends StatelessWidget { + const SearchText({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 40, + decoration: buildBoxDecoration(), + child: TextField( + decoration: CustomInputs.searchInputDecoration( + hint: 'Buscar', icon: Icons.search_outlined), + ), + ); + } + + BoxDecoration buildBoxDecoration() { + return BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.grey.withOpacity(0.1), + ); + } +} diff --git a/lib/ui/shared/widgets/status_item.dart b/lib/ui/shared/widgets/status_item.dart new file mode 100644 index 0000000..2589007 --- /dev/null +++ b/lib/ui/shared/widgets/status_item.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class StatusItem extends StatelessWidget { + final String text; + final Color color; + + const StatusItem({ + super.key, + required this.text, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + text, + style: const TextStyle( + fontStyle: FontStyle.italic, + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/ui/shared/widgets/text_separator.dart b/lib/ui/shared/widgets/text_separator.dart new file mode 100644 index 0000000..6fcf5c0 --- /dev/null +++ b/lib/ui/shared/widgets/text_separator.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class TextSeparator extends StatelessWidget { + final String text; + + const TextSeparator({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 20), + margin: EdgeInsets.only(bottom: 5), + child: Text( + text, + style: GoogleFonts.roboto( + color: Colors.white.withOpacity(0.3), fontSize: 12), + ), + ); + } +} diff --git a/lib/ui/views/blank_view.dart b/lib/ui/views/blank_view.dart new file mode 100644 index 0000000..5d89496 --- /dev/null +++ b/lib/ui/views/blank_view.dart @@ -0,0 +1,24 @@ +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:flutter/material.dart'; + +class BlankView extends StatelessWidget { + const BlankView({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + child: ListView( + physics: const ClampingScrollPhysics(), + children: [ + Text('Blank view', style: CustomLabels.h1), + SizedBox(height: 10), + WhiteCard( + title: 'Blank', + child: Text('Blank View'), + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/calendar_view.dart b/lib/ui/views/calendar_view.dart new file mode 100644 index 0000000..462e32c --- /dev/null +++ b/lib/ui/views/calendar_view.dart @@ -0,0 +1,383 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/providers/professional_form_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/calendar_services_provider.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/utils/time_of_day_extension.dart'; +import 'package:prosapp_web_app/utils/time_of_day_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:table_calendar/table_calendar.dart'; + +class CalendarView extends StatefulWidget { + final String professionalId; + + const CalendarView({super.key, required this.professionalId}); + + @override + State createState() => _CalendarViewState(); +} + +class _CalendarViewState extends State { + List? _services; + DateTime today = DateTime.now(); + late int numDay; + + @override + void initState() { + super.initState(); + + today = DateTime.utc(today.year, today.month, today.day); + numDay = today.weekday; + + _fetchProfessionalAndServices(); + } + + void _fetchProfessionalAndServices() async { + final professionalFormProvider = + Provider.of(context, listen: false); + final servicesProvider = + Provider.of(context, listen: false); + final proProvider = + Provider.of(context, listen: false); + + final professional = + await proProvider.getProfessional(widget.professionalId); + professionalFormProvider.setProfesional(professional); + + final services = + await servicesProvider.getServicesForProfessional(professional.id); + setState(() { + _services = services; + }); + } + + void _onDaySelected(DateTime day, DateTime focusedDay) { + setState(() { + today = day; + numDay = today.weekday; + }); + _fetchProfessionalAndServices(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, professionalFormProvider, child) { + if (professionalFormProvider.profesional == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final profesional = professionalFormProvider.profesional!; + + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + title: 'Calendario', + child: Column( + children: [ + TableCalendar( + locale: 'es_CO', + firstDay: DateTime.now(), + lastDay: DateTime.now().add(const Duration(days: 180)), + focusedDay: today, + availableGestures: AvailableGestures.all, + onDaySelected: _onDaySelected, + selectedDayPredicate: (day) => isSameDay(day, today), + ), + const Divider(height: 0), + SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 8, + ), + child: Text( + DateFormat('dd MMMM yyyy', 'es').format(today), + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const Divider(height: 0), + Column( + children: [ + ..._rangesItems( + _getScheduleFromNumDay(numDay, profesional), + context), + const SizedBox(height: 8), + ], + ), + ], + ), + ), + ), + ), + ], + ); + }); + } + + ScheduleEntity? _getScheduleFromNumDay( + int numDay, Profesional userProfessional) { + switch (numDay) { + case 1: + return userProfessional.schedules.monday; + case 2: + return userProfessional.schedules.tuesday; + case 3: + return userProfessional.schedules.wednesday; + case 4: + return userProfessional.schedules.thursday; + case 5: + return userProfessional.schedules.friday; + case 6: + return userProfessional.schedules.saturday; + case 7: + return userProfessional.schedules.sunday; + default: + return null; + } + } + + List _rangesItems(ScheduleEntity? schedule, BuildContext context) { + if (schedule == null) { + return [ + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("No hay horarios disponibles"), + ) + ]; + } + if (!schedule.enabled) { + return [ + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("No hay horarios disponibles"), + ) + ]; + } + + if (schedule.continuousDay) { + if (schedule.range1Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) { return [ + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("No hay horarios disponibles"), + ) + ]; + } + + List ranges = TimeOfDayUtils.genRanges( + schedule.range1Hour1!, + schedule.range2Hour2!, + ); + + return rangesItemList(ranges, _services, today, context); + } else { + if (schedule.range1Hour1 == null || schedule.range1Hour2 == null || schedule.range2Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range1Hour2!) >= 0 || schedule.range2Hour1!.compareTo(schedule.range2Hour2!) >= 0) { + return [ + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("No hay horarios disponibles"), + ) + ]; + } + + List ranges1 = TimeOfDayUtils.genRanges( + schedule.range1Hour1!, + schedule.range1Hour2!, + ); + List ranges2 = TimeOfDayUtils.genRanges( + schedule.range2Hour1!, + schedule.range2Hour2!, + ); + + return [ + ...rangesItemList(ranges1, _services, today, context), + ...rangesItemList(ranges2, _services, today, context), + ]; + } + } + + bool _isHora1Ocupada( + TimeOfDay hora1, List? events, DateTime selectedDay) { + if (events != null) { + for (Service event in events) { + if (selectedDay.toIso8601String().split('T').first == event.day) { + if (hora1 == event.range1Hour1) { + return true; + } + } + } + } + return false; + } + + List rangesItemList(List ranges, List? events,DateTime selectedDay, BuildContext context) { + final currentDateTime = DateTime.now(); + + return ranges.map((time) { + final selectedDateTime = DateTime( + selectedDay.year, + selectedDay.month, + selectedDay.day, + time.hour, + time.minute, + ); + + if (selectedDateTime + .isBefore(currentDateTime.add(const Duration(hours: 3)))) { + return Card( + elevation: 4, + margin: const EdgeInsets.only(top: 15, left: 10, right: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.grey, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + subtitle: const Text( + 'No disponible', + style: TextStyle( + color: Colors.red, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + if (_isHora1Ocupada(time, events, selectedDay)) { + return Card( + elevation: 4, + margin: const EdgeInsets.only(top: 15, left: 10, right: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () { + if (events != null) { + for (Service event in events) { + if (selectedDay.toIso8601String().split('T').first == + event.day) { + if (time == event.range1Hour1) { + NotificationsService.showSnackbar('Ocupado'); + } + } + } + } + }, + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.red, Colors.red], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + subtitle: const Text( + 'Ocupado', + style: TextStyle( + color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold), + ), + ), + ); + } else { + return Card( + elevation: 4, + margin: const EdgeInsets.only(top: 15, left: 10, right: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () { + Navigator.pop(context, [selectedDay, time]); + }, + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue, Colors.green], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + subtitle: const Text( + 'Disponible', + style: TextStyle( + color: Colors.green, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + }).toList(); + } +} diff --git a/lib/ui/views/chat_view.dart b/lib/ui/views/chat_view.dart new file mode 100644 index 0000000..c3562fd --- /dev/null +++ b/lib/ui/views/chat_view.dart @@ -0,0 +1,350 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/message_entity.dart'; +import 'package:prosapp_web_app/providers/chat_provider.dart'; +import 'package:prosapp_web_app/providers/score_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:provider/provider.dart'; + +class ChatView extends StatelessWidget { + final String type; + final String serviceId; + final String professionalId; + + const ChatView({ + super.key, + required this.type, + required this.serviceId, + required this.professionalId, + }); + + @override + Widget build(BuildContext context) { + final chatProvider = Provider.of(context); + final servicesProvider = + Provider.of(context, listen: false); + final scoreProvider = Provider.of(context, listen: false); + + scoreProvider.loadReputation(professionalId); + + return StreamBuilder( + stream: chatProvider.getChat(serviceId), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + + final chat = snapshot.data; + + if (chat == null) { + return const Center( + child: Text('Chat no encontrado'), + ); + } + + final service = servicesProvider.service?.service; + final user = servicesProvider.service?.user; + final reputation = scoreProvider.reputation; + + if (service == null || user == null) { + return const Center(child: CircularProgressIndicator()); + } + + final image = (user.picture == '' || user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: user.picture!, + ); + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 15), + color: const Color(0xFFD6F4FF), + alignment: Alignment.topCenter, + child: Row( + children: [ + const SizedBox(width: 20), + SizedBox( + width: 85, + height: 85, + child: ClipOval(child: image), + ), + const SizedBox(width: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user.name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + RatingBar.builder( + initialRating: type == 'user' + ? calculoRating(reputation.averagePro) + : calculoRating(reputation.average), + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 25, + maxRating: 5, + itemBuilder: (context, _) => const Icon( + Icons.star, + color: Color(0xFF2BA4EC), + ), + onRatingUpdate: (rating) {}, + ignoreGestures: true, + ), + const SizedBox(width: 5), + if (type == 'user') + Text( + '${reputation.averagePro.toStringAsFixed(1)} (${reputation.totalPro.toString()})', + ), + if (type == 'professional') + Text( + '${reputation.average.toStringAsFixed(1)} (${reputation.total.toString()})', + ), + ], + ) + ], + ) + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 10), + reverse: true, + child: Column( + children: _messagesList(chat.messages), + ), + ), + ), + _MessageInput( + serviceId: serviceId, + userId: FirebaseAuth.instance.currentUser!.uid), + ], + ), + ), + ); + }, + ); + } + + double calculoRating(double average) { + String numeroString = average.toString(); + List partes = numeroString.split('.'); + int parteEntera = int.parse(partes[0]); + int parteFraccionaria = partes.length > 1 ? int.parse(partes[1]) : 0; + + if (parteFraccionaria >= 3) { + parteFraccionaria = 5; + } else { + parteFraccionaria = 0; + } + + // Unir la parte entera y fraccionaria y convertirlo nuevamente a double + double resultado = double.parse('$parteEntera.$parteFraccionaria'); + return resultado; + } + + List _messagesList(List messages) { + return messages + .map( + (e) => e.ownerId != FirebaseAuth.instance.currentUser!.uid + ? ListTile( + title: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(right: 60), + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 16), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Text( + e.content, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + _getFechaHoraFormateada(e.createdAt), + style: + const TextStyle(color: Colors.grey, fontSize: 11), + ), + ), + ], + ), + ) + : ListTile( + title: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.only(left: 60), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: const BoxDecoration( + color: Color(0xFFD5EFFF), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Text( + e.content, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + _getFechaHoraFormateada(e.createdAt), + style: const TextStyle( + color: Colors.grey, + fontSize: 11, + ), + ), + ), + ], + ), + ), + ) + .toList(); + } + + String _getFechaHoraFormateada(DateTime fecha) { + final now = DateTime.now(); + final yesterday = now.subtract(const Duration(days: 1)); + final formatter = DateFormat('h:mm a'); + + if (fecha.day == now.day && + fecha.month == now.month && + fecha.year == now.year) { + return 'Hoy - ${formatter.format(fecha)}'; + } else if (fecha.day == yesterday.day && + fecha.month == yesterday.month && + fecha.year == yesterday.year) { + return 'Ayer - ${formatter.format(fecha)}'; + } else { + return '${DateFormat('dd/MM/yyyy').format(fecha)} - ${formatter.format(fecha)}'; + } + } + + void navigateTo(String routeName) { + NavigationService.replaceTo(routeName); + SideMenuProvider.closeMenu(); + } +} + +class _MessageInput extends StatefulWidget { + final String serviceId; + final String userId; + + const _MessageInput({ + required this.serviceId, + required this.userId, + }); + + @override + State<_MessageInput> createState() => _MessageInputState(); +} + +class _MessageInputState extends State<_MessageInput> { + final _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final chatProvider = Provider.of(context, listen: false); + + void sendMessage() { + if (_controller.text.trim().isNotEmpty) { + final message = MessageEntity( + ownerId: widget.userId, + content: _controller.text.trim(), + createdAt: DateTime.now(), + ); + chatProvider.sendMessage(widget.serviceId, message); + _controller.clear(); + } + } + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: InputDecoration( + hintText: 'Mensaje', + hintStyle: TextStyle(color: Colors.grey[600], fontSize: 16), + border: OutlineInputBorder( + borderSide: + const BorderSide(color: Colors.grey, width: 1.0), + borderRadius: BorderRadius.circular(50)), + focusedBorder: OutlineInputBorder( + borderSide: + const BorderSide(color: Colors.grey, width: 1.0), + borderRadius: BorderRadius.circular(50)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 15), + filled: true, + fillColor: Colors.grey[200], + ), + onSubmitted: (value) => sendMessage(), + ), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: () => sendMessage(), + child: Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: const Color(0xFF2BA4EC), + borderRadius: BorderRadius.circular(30), + ), + child: const Center( + child: Icon( + Icons.send, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/dashboard_view.dart b/lib/ui/views/dashboard_view.dart new file mode 100644 index 0000000..35906e4 --- /dev/null +++ b/lib/ui/views/dashboard_view.dart @@ -0,0 +1,398 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/service_location_preferences.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/models/usuario_profesional.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/cities_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/utils/local_notifications.dart'; +import 'package:prosapp_web_app/utils/network_utility.dart'; +import 'package:prosapp_web_app/utils/time_of_day_extension.dart'; +import 'package:provider/provider.dart'; + +class DashboardView extends StatefulWidget { + const DashboardView({super.key}); + + @override + State createState() => _DashboardViewState(); +} + +class _DashboardViewState extends State { + Usuario? user; + List _placesList = []; + final TextEditingController _addressController = TextEditingController(); + Timer? _debounce; + UsuarioProfesional? selectedProfessional; + String? selectedProfessionalName; + DateTime? selectedDay; + TimeOfDay? selectedHour; + + @override + void initState() { + super.initState(); + + final authProvider = Provider.of(context, listen: false); + + setState(() { + user = authProvider.user; + }); + } + + void placeAutoComplete(String query, String _coords) async { + Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", { + "input": query, + "location": _coords, + }); + + String? response = await NetworkUtility.fetchUrl(uri); + + if (response != null) { + if (mounted) { + setState(() { + _placesList = jsonDecode(response.toString())['results']; + }); + } + } + } + + Future _selectProfessional(BuildContext context) async { + try { + List result = await NavigationService.navigateToFuture( + Flurorouter.professionalsRoute); + + final UsuarioProfesional selectedProfessional = result[0]; + + print('selectedProfessional: $selectedProfessional'); + + setState(() { + selectedDay = result[1]; + selectedHour = result[2]; + selectedProfessionalName = selectedProfessional.user.name; + this.selectedProfessional = selectedProfessional; + _addressController.text = selectedProfessional.professionalInfo.address; + }); + } catch (e) { + print('debugeando $e'); + } + } + + @override + void dispose() { + _debounce?.cancel(); + _addressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (user == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + bool isUserComplete() { + return user!.name != '' && + user!.email != '' && + user!.phone != '' && + user!.city != ''; + } + + final citiesProvider = Provider.of(context); + + if (citiesProvider.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final _coords = citiesProvider.getCoordsOfCity(user!.city!); + + _createService() async { + print('debug ${selectedProfessional?.user.id ?? 'user.id'}'); + print('debug ${user!.id ?? 'user.id'}'); + print('debug ${_addressController.text ?? 'user.id'}'); + + try { + double latitude = 0.0; + double longitude = 0.0; + + print('debug 1'); + + Service service = Service( + id: null, + professionalId: selectedProfessional!.user.id, + professionalScored: false, + userId: user!.id, + userScored: false, + address: _addressController.text, + aditionalAddress: '', + latitude: latitude, + longitude: longitude, + day: selectedDay.toString(), + createdAt: Timestamp.now(), + description: '', + range1Hour1: selectedHour!, + range1Hour2: selectedHour!.add(hour: 2), + rate: '', + status: ServiceStatus.pending, + location: ServiceLocationPreferences.delivery, + ); + + print('debug 1'); + + await FirebaseFirestore.instance + .collection('services') + .add(service.toDocument()); + + print('debug 2'); + + NotificationsService.showSnackbar('Servicio solicitado exitosamente'); + + if (selectedProfessional != null) { + if (selectedProfessional!.user.token != null) { + LocalNotifications.sendPushNotification( + selectedProfessional!.user.token!, + 'Nuevo servicio', + 'Tienes una nueva solicitud de servicio pendiente', + ); + } + } + + _addressController.clear(); + setState(() { + selectedProfessional = null; + selectedProfessionalName = null; + selectedDay = null; + selectedHour = null; + }); + } catch (e) { + NotificationsService.showSnackBarError( + '$e Error al solicitar el servicio, intenta de nuevo'); + } + } + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Stack( + children: [ + ListView( + physics: const ClampingScrollPhysics(), + children: [ + WhiteCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + isUserComplete() + ? TextFormField( + controller: _addressController, + onChanged: (value) { + if (_debounce?.isActive ?? false) + _debounce?.cancel(); + + _debounce = Timer( + const Duration(milliseconds: 500), () { + String modifiedValue = + value.replaceAll(' ', '_'); + placeAutoComplete(modifiedValue, _coords); + }); + }, + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa tu dirección', + label: 'Dirección', + icon: Icons.location_on, + ), + ) + : GestureDetector( + onTap: () => NotificationsService.showSnackBarError( + 'Completa tu perfil para solicitar un servicio'), + child: AbsorbPointer( + child: TextFormField( + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa tu dirección', + label: 'Dirección', + icon: Icons.location_on, + ), + ), + ), + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () => isUserComplete() + ? _selectProfessional(context) + : NotificationsService.showSnackBarError( + 'Completa tu perfil para solicitar un servicio'), + child: AbsorbPointer( + child: TextFormField( + controller: TextEditingController( + text: selectedProfessionalName, + ), + decoration: CustomInputs.formInputDecoration( + hint: 'Selecciona un Profesional', + label: 'Profesional', + icon: Icons.person_rounded, + ), + ), + ), + ), + const SizedBox(height: 10), + if (selectedDay != null && selectedHour != null) ...[ + TextFormField( + readOnly: true, + controller: TextEditingController( + text: selectedDay == null + ? '' + : DateFormat('dd/MM/yyyy').format(selectedDay!), + ), + decoration: CustomInputs.formInputDecoration( + hint: 'Fecha', + label: 'Fecha', + icon: Icons.calendar_month, + ), + ), + const SizedBox(height: 10), + TextFormField( + readOnly: true, + controller: TextEditingController( + text: selectedHour == null + ? '' + : ScheduleEntity.getFormatTime(selectedHour), + ), + decoration: CustomInputs.formInputDecoration( + hint: 'Hora', + label: 'Hora', + icon: Icons.watch_later_outlined, + ), + ), + const SizedBox(height: 10), + ], + const SizedBox(height: 10), + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 230), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + if (!isUserComplete()) { + NotificationsService.showSnackBarError( + 'Completa tu perfil para solicitar un servicio'); + + return; + } + + _createService(); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all( + Colors.transparent), + ), + child: const Text( + 'Solicitar cita', + style: TextStyle(color: Colors.white), + ), + ), + if (selectedDay != null && + selectedHour != null && + selectedProfessional != null) ...[ + const SizedBox(width: 10), + ElevatedButton( + onPressed: () async { + _addressController.clear(); + selectedProfessional = null; + selectedDay = null; + selectedHour = null; + + setState(() {}); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.red, + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5)))), + shadowColor: WidgetStateProperty.all( + Colors.transparent)), + child: const Icon( + Icons.close_rounded, + color: Colors.white, + ), + ), + ], + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ], + ), + Positioned( + left: 20, + right: 20, + top: 80, + child: _placesList.isNotEmpty + ? Material( + elevation: 5.0, + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: _placesList.length, + itemBuilder: (context, index) { + return ListTile( + title: + Text(_placesList[index]['formatted_address']), + dense: true, + visualDensity: VisualDensity.compact, + onTap: () { + setState(() { + _addressController.text = + _placesList[index]['formatted_address']; + _placesList = []; + }); + }, + ); + }, + ), + ), + ) + : Container(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/views/email_view.dart b/lib/ui/views/email_view.dart new file mode 100644 index 0000000..320a0d8 --- /dev/null +++ b/lib/ui/views/email_view.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/email_form_provider.dart'; +import 'package:prosapp_web_app/providers/phone_form_provider.dart'; +import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:provider/provider.dart'; + +class EmailView extends StatelessWidget { + const EmailView({super.key}); + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + final RegExp emailRegex = + RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"); + + RegExp passwordRegex = RegExp(r'^(?=.*?[0-9])'); + + return ChangeNotifierProvider( + create: (_) => EmailFormProvider(), + child: Builder(builder: (context) { + final emailFormProvider = + Provider.of(context, listen: false); + + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 40), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 370), + child: Form( + autovalidateMode: + AutovalidateMode.onUserInteraction, + key: emailFormProvider.formKey, + child: Column( + children: [ + TextFormField( + controller: _emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Ingresa un email'; + } + + if (!emailRegex.hasMatch(value)) { + return 'Ingresa un email válido'; + } + return null; + }, + onChanged: (email) => + emailFormProvider.email = email, + decoration: + CustomInputs.loginInputDecoration( + hint: 'Ingresa tu email', + label: 'Email', + icon: Icons.email_outlined, + ), + ), + const SizedBox(height: 20), + TextFormField( + controller: _passwordController, + obscureText: true, + onChanged: (value) => + emailFormProvider.password = value, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Ingresa una contraseña'; + } + + if (value.length < 6) { + return 'La contraseña debe tener al menos 6 caracteres'; + } + + if (!passwordRegex.hasMatch(value)) { + return 'La contraseña debe tener al menos un número'; + } + return null; + }, + decoration: + CustomInputs.loginInputDecoration( + hint: 'Ingresa tu contraseña', + label: 'Contraseña', + icon: Icons.lock_outline, + ), + ), + const SizedBox(height: 20), + TextFormField( + obscureText: true, + controller: _confirmPasswordController, + onChanged: (value) => + emailFormProvider.password = value, + validator: (value) { + if (value != _passwordController.text) { + return 'Las contraseñas no coinciden'; + } + if (value!.isEmpty) { + return 'La contraseña es obligatoria'; + } + return null; + }, + decoration: + CustomInputs.loginInputDecoration( + hint: 'Confirma tu contraseña', + label: 'Confirmar contraseña', + icon: Icons.lock_outline, + ), + ), + const SizedBox(height: 20), + CustomOutlinedButton( + onPressed: () async { + final isValid = + emailFormProvider.validateForm(); + if (isValid) { + await authProvider.addEmailAndPassword( + emailFormProvider.email, + emailFormProvider.password); + } + }, + text: "Guardar", + color: Colors.blue, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + }), + ); + } +} diff --git a/lib/ui/views/login_view.dart b/lib/ui/views/login_view.dart new file mode 100644 index 0000000..d672bbe --- /dev/null +++ b/lib/ui/views/login_view.dart @@ -0,0 +1,125 @@ +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/login_form_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart'; +import 'package:prosapp_web_app/ui/buttons/link_text.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class LoginView extends StatelessWidget { + const LoginView({super.key}); + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + + return ChangeNotifierProvider( + create: (_) => LoginFormProvider(), + child: Builder(builder: (context) { + final loginFormProvider = + Provider.of(context, listen: false); + + return Container( + margin: const EdgeInsets.only(top: 100), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 370), + child: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + key: loginFormProvider.formKey, + child: Column( + children: [ + // Email + TextFormField( + onFieldSubmitted: (_) => + onFormSubmit(loginFormProvider, authProvider), + validator: (value) { + if (!EmailValidator.validate(value ?? '')) { + return 'Email no válido'; + } + return null; + }, + onChanged: (value) => loginFormProvider.email = value, + decoration: CustomInputs.loginInputDecoration( + hint: 'Ingresa tu correo', + label: 'Email', + icon: Icons.email_outlined, + ), + ), + + const SizedBox(height: 20), + + // Password + TextFormField( + onFieldSubmitted: (_) => + onFormSubmit(loginFormProvider, authProvider), + validator: (value) { + if (value == null || value.isEmpty) { + return "Ingresa tu contraseña"; + } + + if (value.length < 8) { + return "La contraseña debe tener al menos 8 caracteres"; + } + + return null; // Válido + }, + onChanged: (value) => loginFormProvider.password = value, + obscureText: true, + decoration: CustomInputs.loginInputDecoration( + hint: 'Ingresa tu contraseña', + label: 'Contraseña', + icon: Icons.lock_outline, + ), + ), + + const SizedBox(height: 20), + + CustomOutlinedButton( + onPressed: () => + onFormSubmit(loginFormProvider, authProvider), + text: "Ingresar", + color: Colors.blue, + ), + const SizedBox(height: 20), + + LinkText( + text: "Nueva cuenta", + onPressed: () { + Navigator.pushReplacementNamed( + context, Flurorouter.registerRoute); + }, + ), + const SizedBox(height: 5), + + LinkText( + text: "Entrar con celular", + onPressed: () { + Navigator.pushReplacementNamed( + context, Flurorouter.phoneLoginRoute); + }, + ), + ], + ), + ), + ), + ), + ); + }), + ); + } + + void onFormSubmit( + LoginFormProvider loginFormProvider, AuthProvider authProvider) async { + final isValid = loginFormProvider.validateForm(); + if (isValid) { + await authProvider.login( + loginFormProvider.email, + loginFormProvider.password, + ); + } + } +} diff --git a/lib/ui/views/no_page_found_view.dart b/lib/ui/views/no_page_found_view.dart new file mode 100644 index 0000000..2b4a560 --- /dev/null +++ b/lib/ui/views/no_page_found_view.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class NoPageFoundView extends StatelessWidget { + const NoPageFoundView({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + '404 - No Page Found', + style: GoogleFonts.montserratAlternates( + fontSize: 50, fontWeight: FontWeight.bold), + ), + ); + } +} diff --git a/lib/ui/views/phone_login_view.dart b/lib/ui/views/phone_login_view.dart new file mode 100644 index 0000000..351c330 --- /dev/null +++ b/lib/ui/views/phone_login_view.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/phone_form_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart'; +import 'package:prosapp_web_app/ui/buttons/link_text.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:provider/provider.dart'; + +class PhoneLoginView extends StatelessWidget { + const PhoneLoginView({super.key}); + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + + final TextEditingController _phoneController = TextEditingController(); + + return ChangeNotifierProvider( + create: (_) => PhoneFormProvider(), + child: Builder(builder: (context) { + final phoneFormProvider = + Provider.of(context, listen: false); + + return Container( + margin: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 370), + child: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + key: phoneFormProvider.formKey, + child: Column( + children: [ + TextFormField( + controller: _phoneController, + validator: (value) { + if (value == null || + value.isEmpty || + value.length < 10) { + return 'Ingresa un número de teléfono válido'; + } + return null; + }, + onChanged: (value) { + phoneFormProvider.phone = '+57${value.trim()}'; + }, + keyboardType: TextInputType.phone, + decoration: CustomInputs.loginInputDecoration( + hint: 'Ingresa tu número de teléfono', + label: 'Número de Teléfono', + icon: Icons.phone_android_outlined, + ), + ), + const SizedBox(height: 20), + CustomOutlinedButton( + onPressed: () async { + final isValid = phoneFormProvider.validateForm(); + if (isValid) { + await authProvider + .verifyPhoneNumber(phoneFormProvider.phone); + + showDialog( + context: context, + builder: (context) => + _buildOtpModal(context, authProvider), + ); + } + }, + text: "Enviar código", + color: Colors.blue, + ), + const SizedBox(height: 20), + LinkText( + text: "Entrar con correo", + onPressed: () { + Navigator.pushReplacementNamed( + context, Flurorouter.loginRoute); + }, + ), + ], + ), + ), + ), + ), + ); + }), + ); + } + + Widget _buildOtpModal(BuildContext context, AuthProvider authProvider) { + final _otpController = TextEditingController(); + + return AlertDialog( + title: const Text('Ingresar código OTP'), + content: TextField( + controller: _otpController, + decoration: const InputDecoration(labelText: 'Código OTP'), + keyboardType: TextInputType.number, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancelar'), + ), + ElevatedButton( + onPressed: () async { + final otp = _otpController.text.trim(); + + if (otp.isNotEmpty) { + await authProvider.signInWithOTP(otp); + Navigator.of(context).pop(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Por favor ingresa el código OTP')), + ); + } + }, + child: const Text('Verificar OTP'), + ), + ], + ); + } +} diff --git a/lib/ui/views/phone_view.dart b/lib/ui/views/phone_view.dart new file mode 100644 index 0000000..0d6252e --- /dev/null +++ b/lib/ui/views/phone_view.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/phone_form_provider.dart'; +import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:provider/provider.dart'; + +class PhoneView extends StatelessWidget { + const PhoneView({super.key}); + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + + final TextEditingController _phoneController = TextEditingController(); + + return ChangeNotifierProvider( + create: (_) => PhoneFormProvider(), + child: Builder(builder: (context) { + final phoneFormProvider = + Provider.of(context, listen: false); + + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 40), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 370), + child: Form( + autovalidateMode: + AutovalidateMode.onUserInteraction, + key: phoneFormProvider.formKey, + child: Column( + children: [ + TextFormField( + controller: _phoneController, + validator: (value) { + if (value == null || + value.isEmpty || + value.length < 10) { + return 'Ingresa un número de teléfono válido'; + } + return null; + }, + onChanged: (value) { + phoneFormProvider.phone = + '+57${value.trim()}'; + }, + keyboardType: TextInputType.phone, + decoration: + CustomInputs.loginInputDecoration( + hint: 'Ingresa tu número de teléfono', + label: 'Número de Teléfono', + icon: Icons.phone_android_outlined, + ), + ), + const SizedBox(height: 20), + CustomOutlinedButton( + onPressed: () async { + final isValid = + phoneFormProvider.validateForm(); + if (isValid) { + await authProvider + .verifyPhoneNumberForLink( + phoneFormProvider.phone); + + showDialog( + context: context, + builder: (context) => _buildOtpModal( + context, authProvider), + ); + } + }, + text: "Enviar código", + color: Colors.blue, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + }), + ); + } + + Widget _buildOtpModal(BuildContext context, AuthProvider authProvider) { + final _otpController = TextEditingController(); + + return AlertDialog( + title: const Text('Ingresar código OTP'), + content: TextField( + controller: _otpController, + decoration: const InputDecoration(labelText: 'Código OTP'), + keyboardType: TextInputType.number, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancelar'), + ), + ElevatedButton( + onPressed: () async { + final otp = _otpController.text.trim(); + + if (otp.isNotEmpty) { + await authProvider.linkPhoneWithOTP(otp); + Navigator.of(context).pop(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Por favor ingresa el código OTP')), + ); + } + }, + child: const Text('Verificar OTP'), + ), + ], + ); + } +} diff --git a/lib/ui/views/professional_calendar_view.dart b/lib/ui/views/professional_calendar_view.dart new file mode 100644 index 0000000..df2233e --- /dev/null +++ b/lib/ui/views/professional_calendar_view.dart @@ -0,0 +1,356 @@ +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/profesional.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/professional_form_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/utils/time_of_day_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:flutter/material.dart'; + +class ProfessionalCalendarView extends StatefulWidget { + const ProfessionalCalendarView({super.key}); + + @override + State createState() => + _ProfessionalCalendarViewState(); +} + +class _ProfessionalCalendarViewState extends State { + DateTime today = DateTime.now(); + late int numDay; + + List? _services; + + Usuario? user; + + @override + void initState() { + super.initState(); + numDay = today.weekday; + _fetchProfessionalAndServices(); + } + + void _fetchProfessionalAndServices() async { + final authProvider = Provider.of(context, listen: false); + final professionalFormProvider = Provider.of(context, listen: false); + final servicesProvider = Provider.of(context, listen: false); + + + + final proProvider = Provider.of(context, listen: false); + + final professional = await proProvider.getProfessional(authProvider.user!.id); + professionalFormProvider.setProfesional(professional); + + final services = await servicesProvider.getServicesForProfessional(professional.id); + setState(() { + _services = services; + user = authProvider.user; + }); + } + + void _onDaySelected(DateTime day, DateTime focusedDay) { + setState(() { + today = day; + numDay = today.weekday; + }); + _fetchProfessionalAndServices(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, professionalFormProvider, child) { + if (professionalFormProvider.profesional == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final profesional = professionalFormProvider.profesional!; + + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + title: 'Calendario', + child: Column( + children: [ + TableCalendar( + locale: 'es_CO', + firstDay: DateTime.now(), + lastDay: DateTime.utc(2030, 3, 14), + focusedDay: today, + availableGestures: AvailableGestures.all, + onDaySelected: _onDaySelected, + selectedDayPredicate: (day) => isSameDay(day, today), + ), + const Divider(height: 0), + SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 8, + ), + child: Text( + DateFormat('dd MMMM yyyy', 'es').format(today), + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const Divider(height: 0), + Column( + children: [ + ..._rangesItems(_getScheduleFromNumDay(numDay, profesional),context), + ], + ), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } + + ScheduleEntity? _getScheduleFromNumDay(int numDay, Profesional userProfessional) { + switch (numDay) { + case 1: + return userProfessional.schedules.monday; + case 2: + return userProfessional.schedules.tuesday; + case 3: + return userProfessional.schedules.wednesday; + case 4: + return userProfessional.schedules.thursday; + case 5: + return userProfessional.schedules.friday; + case 6: + return userProfessional.schedules.saturday; + case 7: + return userProfessional.schedules.sunday; + default: + return null; + } + } + + List _rangesItems(ScheduleEntity? schedule, BuildContext context) { + if (schedule == null || + !schedule.enabled || + schedule.range1Hour1 == null || + schedule.range2Hour2 == null) { + return [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 25), + child: Text("No hay horarios disponibles"), + ) + ]; + } + + if (schedule.continuousDay) { + List ranges = TimeOfDayUtils.genRanges( + schedule.range1Hour1!, + schedule.range2Hour2!, + ); + + return rangesItemList(ranges, _services, today, context); + } + + List ranges1 = TimeOfDayUtils.genRanges( + schedule.range1Hour1!, + schedule.range1Hour2!, + ); + List ranges2 = TimeOfDayUtils.genRanges( + schedule.range2Hour1!, + schedule.range2Hour2!, + ); + + return [ + ...rangesItemList(ranges1, _services, today, context), + ...rangesItemList(ranges2, _services, today, context), + ]; + } + + bool _isHora1Ocupada( + TimeOfDay hora1, List? events, DateTime selectedDay) { + if (events != null) { + for (Service event in events) { + if (selectedDay.toIso8601String().split('T').first == event.day) { + if (hora1 == event.range1Hour1) { + return true; + } + } + } + } + return false; + } + + List rangesItemList(List ranges, List? events, + DateTime selectedDay, BuildContext context) { + return ranges.map((time) { + if (_isHora1Ocupada(time, events, selectedDay)) { + return Card( + elevation: 4, + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () { + if (events != null) { + for (Service event in events) { + if (selectedDay.toIso8601String().split('T').first == + event.day) { + if (time == event.range1Hour1) { + if (event.userId == event.professionalId) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Horario ocupado por ti'), + ), + ); + } else { + // Navigator.push( + // context, + // CupertinoPageRoute( + // builder: (context) => ProfessionalServiceScreen( + // serviceId: event.id!, + // ), + // ), + // ); + } + } + } + } + } + }, + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.red, Colors.red], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + subtitle: const Text( + 'Ocupado', + style: TextStyle( + color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold), + ), + ), + ); + } else { + return Card( + elevation: 4, + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Reservar hora'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '¿Estás seguro de que deseas reservar a las ${ScheduleEntity.getFormatTime(time)} del ${DateFormat('dd-MM-yyyy').format(today)}?', + ), + const SizedBox(height: 10), + const Text( + '⚠️ Esta acción no se puede deshacer ⚠️', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + }, + child: const Text('No, cancelar'), + ), + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + + Navigator.pop(context); + }, + child: const Text('Sí, reservar'), + ), + ], + ); + }, + ); + }, + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue, Colors.green], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + subtitle: const Text( + 'Disponible', + style: TextStyle( + color: Colors.green, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + }).toList(); + } +} diff --git a/lib/ui/views/professional_profile_view.dart b/lib/ui/views/professional_profile_view.dart new file mode 100644 index 0000000..32dc3cf --- /dev/null +++ b/lib/ui/views/professional_profile_view.dart @@ -0,0 +1,535 @@ +import 'dart:typed_data'; +import 'package:file_picker/file_picker.dart'; +import 'package:prosapp_web_app/models/location_preferences.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/professional_form_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/profile_form_provider.dart'; +import 'package:prosapp_web_app/providers/settings_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ProfessionalProfileView extends StatefulWidget { + const ProfessionalProfileView({super.key}); + + @override + State createState() => + _ProfessionalProfileViewState(); +} + +class _ProfessionalProfileViewState extends State { + Usuario? user; + late ProfessionalFormProvider professionalFormProvider; + + @override + void initState() { + super.initState(); + + final authProvider = Provider.of(context, listen: false); + final profileFormProvider = + Provider.of(context, listen: false); + professionalFormProvider = + Provider.of(context, listen: false); + + final proProvider = + Provider.of(context, listen: false); + + proProvider.getProfessional(authProvider.user!.id).then((value) { + professionalFormProvider.setProfesional(value); + }); + + profileFormProvider.user = authProvider.user; + setState(() { + user = authProvider.user; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < 700) { + return ListView( + physics: const ClampingScrollPhysics(), + children: const [SizedBox(height: 10), _ProfileViewBody()], + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: ListView( + physics: const ClampingScrollPhysics(), + children: const [SizedBox(height: 10), _ProfileViewBody()], + ), + ); + } + }); + } +} + +class _ProfileViewBody extends StatelessWidget { + const _ProfileViewBody(); + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: const Column( + children: [ + _AvatarContainer(containerFull: true), + _ProfileViewForm(), + ], + ), + ), + ); + } +} + +class _ProfileViewForm extends StatefulWidget { + const _ProfileViewForm(); + + @override + State<_ProfileViewForm> createState() => _ProfileViewFormState(); +} + +class _ProfileViewFormState extends State<_ProfileViewForm> { + bool _serviceDeliverySwitchValue = false; + bool _serviceSiteSwitchValue = false; + bool _serviceRateSwitchValue = false; + + bool first = true; + + @override + Widget build(BuildContext context) { + final profileFormProvider = Provider.of(context); + final settingsProvider = Provider.of(context); + final user = profileFormProvider.user!; + + return Consumer( + builder: (context, professionalFormProvider, child) { + if (professionalFormProvider.profesional == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (settingsProvider.settings == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final settings = settingsProvider.settings!; + final profesional = professionalFormProvider.profesional!; + if (first) { + first = false; + + if (settings.tarifas) { + _serviceRateSwitchValue = profesional.ratePreferences; + } else { + _serviceRateSwitchValue = false; + } + + if (profesional.locationPreferences == LocationPreferences.both) { + _serviceSiteSwitchValue = true; + _serviceDeliverySwitchValue = true; + } else if (profesional.locationPreferences == + LocationPreferences.office) { + _serviceSiteSwitchValue = true; + _serviceDeliverySwitchValue = false; + } else if (profesional.locationPreferences == + LocationPreferences.delivery) { + _serviceSiteSwitchValue = false; + _serviceDeliverySwitchValue = true; + } + } + + return LayoutBuilder(builder: (context, constraints) { + double columnWidth = constraints.maxWidth < 600 ? 95 : 150; + + return WhiteCard( + title: 'Información profesional', + child: Form( + key: professionalFormProvider.profileFormKey, + autovalidateMode: AutovalidateMode.always, + child: Column( + children: [ + const SizedBox(height: 10), + SwitchListTile( + title: const Text('Servicio a domicilio'), + value: _serviceDeliverySwitchValue, + onChanged: (value) { + setState(() { + _serviceDeliverySwitchValue = value; + + if (settings.domicilios == true) { + _serviceDeliverySwitchValue = value; + if (!_serviceDeliverySwitchValue) { + _serviceSiteSwitchValue = true; + } + } else { + _serviceDeliverySwitchValue = false; + } + }); + }, + ), + const SizedBox(height: 10), + SwitchListTile( + title: const Text('Servicio en sitio / consultorio'), + value: _serviceSiteSwitchValue, + onChanged: (value) { + setState(() { + if (settings.domicilios == true) { + _serviceSiteSwitchValue = value; + if (!_serviceSiteSwitchValue) { + _serviceDeliverySwitchValue = true; + } + } else { + _serviceSiteSwitchValue = true; + _serviceDeliverySwitchValue = false; + } + }); + }, + ), + if (_serviceSiteSwitchValue) + Column( + children: [ + TextFormField( + initialValue: profesional.address, + // validator: (value) { + // if (value == null || value.isEmpty) { + // return 'La dirección es obligatoria'; + // } + // return null; + // }, + onChanged: (value) {}, + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa tu dirección', + label: 'Dirección', + icon: Icons.location_on_outlined, + ), + ), + const SizedBox(height: 12), + TextFormField( + initialValue: profesional.aditionalAddress, + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa tu piso / apartamento / conjunto', + label: 'Piso / Apartamento / Conjunto', + icon: Icons.email_outlined, + ), + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + aditionalAddress: value); + }, + ), + ], + ), + if (settings.tarifas == true) ...[ + const SizedBox(height: 20), + SwitchListTile( + title: const Text('Tarifa'), + value: _serviceRateSwitchValue, + onChanged: (value) { + setState(() { + _serviceRateSwitchValue = value; + }); + }, + ), + if (_serviceRateSwitchValue) + Column( + children: [ + TextFormField( + keyboardType: TextInputType.number, + initialValue: profesional.rate, + validator: (value) { + if (value == null || value.isEmpty) { + return 'La tarifa es obligatoria'; + } + final numericRegex = RegExp(r'^[0-9]+$'); + if (!numericRegex.hasMatch(value)) { + return 'Por favor, ingresa solo números'; + } + return null; + }, + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + rate: value); + }, + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa la tarifa del servicio', + label: 'Tarifa', + icon: Icons.attach_money, + ), + ), + ], + ), + ], + const SizedBox(height: 20), + Text('Metodos de pago', style: CustomLabels.h3), + CheckboxListTile( + title: const Text('Datafono'), + value: profesional.paymentMethods.datafono, + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + paymentMethods: profesional.paymentMethods + .copyWith(datafono: value)); + }), + const SizedBox(height: 10), + CheckboxListTile( + title: const Text('Nequi'), + value: profesional.paymentMethods.nequi, + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + paymentMethods: profesional.paymentMethods + .copyWith(nequi: value)); + }), + const SizedBox(height: 10), + CheckboxListTile( + title: const Text('Transferencia bancaria'), + value: profesional.paymentMethods.transferencia, + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + paymentMethods: profesional.paymentMethods + .copyWith(transferencia: value)); + }), + const SizedBox(height: 20), + Text('Horarios de atenciòn', style: CustomLabels.h3), + const SizedBox(height: 10), + GestureDetector( + onTap: () { + NavigationService.replaceTo( + Flurorouter.professionalScheduleRoute); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Table( + columnWidths: { + 0: FixedColumnWidth(columnWidth), + }, + border: + TableBorder.all(color: Colors.grey.withOpacity(0.3)), + children: [ + _buildTableRow( + 'Lunes', profesional.schedules.monday, context), + _buildTableRow( + 'Martes', profesional.schedules.tuesday, context), + _buildTableRow('Miercoles', + profesional.schedules.wednesday, context), + _buildTableRow( + 'Jueves', profesional.schedules.thursday, context), + _buildTableRow( + 'Viernes', profesional.schedules.friday, context), + _buildTableRow( + 'Sabado', profesional.schedules.saturday, context), + _buildTableRow( + 'Domingo', profesional.schedules.sunday, context), + ], + ), + ), + ), + const SizedBox(height: 20), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () async { + LocationPreferences locationPreferences; + + if (_serviceDeliverySwitchValue && + _serviceSiteSwitchValue) { + locationPreferences = LocationPreferences.both; + } else if (_serviceDeliverySwitchValue) { + locationPreferences = LocationPreferences.delivery; + } else if (_serviceSiteSwitchValue) { + locationPreferences = LocationPreferences.office; + } else { + locationPreferences = LocationPreferences.office; + } + + professionalFormProvider.copyProfesionalWith( + locationPreferences: locationPreferences, + ratePreferences: _serviceRateSwitchValue, + ); + + await professionalFormProvider + .updateProfesionalProfileInfo(user.id); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + + ), + shape: + WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text( + 'Guardar', + style: TextStyle(color: Colors.white), + ), + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ); + }); + }); + } + + TableRow _buildTableRow( + String day, ScheduleEntity schedules, BuildContext context) { + return TableRow( + children: [ + TableCell( + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: const BoxDecoration( + color: Colors.blueAccent, + ), + child: Text( + day, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + TableCell( + child: Container( + padding: const EdgeInsets.all(8.0), + child: Text( + timeList(schedules, context), + style: const TextStyle( + color: Colors.black87, + ), + ), + ), + ), + ], + ); + } + + String timeList(ScheduleEntity? schedule, BuildContext context) { + if (schedule == null) { + return 'No hay horarios'; + } + if (!schedule.enabled) { + return 'No hay horarios'; + } + if (schedule.range1Hour1 == null || schedule.range2Hour2 == null) { + return 'No hay horarios'; + } + if (schedule.continuousDay) { + return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}'; + } else { + if (schedule.range1Hour2 == null || schedule.range2Hour1 == null) { + return 'No hay horarios'; + } + + return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range1Hour2)} - ${ScheduleEntity.getFormatTime(schedule.range2Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}'; + } + } +} + +class _AvatarContainer extends StatelessWidget { + final bool containerFull; + + const _AvatarContainer({required this.containerFull}); + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).user!; + final profileFormProvider = Provider.of(context); + + final image = (profileFormProvider.user!.picture == '' || + profileFormProvider.user!.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: profileFormProvider.user!.picture!, + ); + + return WhiteCard( + width: containerFull ? null : 250, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(user.name, style: CustomLabels.h2), + const SizedBox(height: 10), + SizedBox( + width: 160, + height: 160, + child: Stack( + children: [ + SizedBox( + width: 200, + height: 200, + child: ClipOval(child: image), + ), + Positioned( + bottom: 5, + right: 5, + child: Container( + width: 45, + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all(color: Colors.white, width: 5), + ), + child: FloatingActionButton( + onPressed: () async { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + withData: true, + ); + + if (result != null) { + PlatformFile file = result.files.first; + Uint8List? fileBytes = file.bytes; + + if (fileBytes != null) { + NotificationsService.showBusyIndicator(context); + await profileFormProvider + .uploadPicture(fileBytes); + Provider.of(context, listen: false) + .refreshUser(); + + Navigator.pop(context); + } + } else {} + }, + backgroundColor: Colors.indigo, + elevation: 0, + child: const Icon( + Icons.camera_alt_outlined, + size: 20, + color: Colors.white, + ), + ), + ), + ) + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } +} diff --git a/lib/ui/views/professionals_view.dart b/lib/ui/views/professionals_view.dart new file mode 100644 index 0000000..6dbbe1a --- /dev/null +++ b/lib/ui/views/professionals_view.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/usuario_profesional.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:provider/provider.dart'; +import 'package:prosapp_web_app/providers/professionals_provider.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; + +class ProfessionalsView extends StatelessWidget { + const ProfessionalsView({super.key}); + + @override + Widget build(BuildContext context) { + final professionalsProvider = Provider.of(context); + + return LayoutBuilder( + builder: (context, constraints) { + int crossAxisCount; + double childAspectRatio; + + if (constraints.maxWidth >= 1540) { + crossAxisCount = 5; + childAspectRatio = 0.75; + } else if (constraints.maxWidth >= 1000) { + crossAxisCount = 4; + childAspectRatio = 0.65; + } else if (constraints.maxWidth >= 650) { + crossAxisCount = 3; + childAspectRatio = 0.55; + } else if (constraints.maxWidth >= 400) { + crossAxisCount = 2; + childAspectRatio = 0.55; + } else { + crossAxisCount = 1; + childAspectRatio = 0.7; + } + + if (professionalsProvider.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + childAspectRatio: childAspectRatio, + ), + itemCount: professionalsProvider.professionals.length, + itemBuilder: (context, index) { + UsuarioProfesional data = professionalsProvider.professionals[index]; + + final image = (data.user.picture == '' || data.user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: data.user.picture!, + ); + + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () async { + final List result = await NavigationService.navigateToFuture('/dashboard/calendar/${data.user.id}'); + + DateTime day = result[0]; + TimeOfDay time = result[1]; + + Navigator.pop(context, [data, day, time]); + }, + child: WhiteCard( + title: data.user.name.toUpperCase(), + child: Column( + children: [ + Row( + children: [ + const Icon( + Icons.star, + size: 15, + color: Colors.yellow, + ), + const SizedBox(width: 3), + Text( + data.averageScore.toString(), + style: CustomLabels.h4, + ), + ], + ), + Center( + child: SizedBox( + width: 100, + height: 100, + child: ClipOval( + child: image, + ), + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 5), + Row( + children: [ + const Icon(Icons.location_on_outlined, + size: 15), + const SizedBox(width: 2), + Text( + data.user.city ?? '', + textAlign: TextAlign.center, + ), + ], + ), + Row( + children: [ + const Icon(Icons.work_outline, size: 15), + const SizedBox(width: 5), + Text(data.professionalInfo.profession), + ], + ), + const Divider(), + if (data.professionalInfo.paymentMethods.datafono || + data.professionalInfo.paymentMethods.nequi || + data.professionalInfo.paymentMethods + .transferencia) ...[ + const Row( + children: [ + Icon(Icons.payment_outlined, size: 15), + SizedBox(width: 5), + Text('Metodos de pago'), + ], + ), + Visibility( + visible: data + .professionalInfo.paymentMethods.datafono, + child: const Text('- Datafono'), + ), + Visibility( + visible: + data.professionalInfo.paymentMethods.nequi, + child: const Text('- Nequi'), + ), + Visibility( + visible: data.professionalInfo.paymentMethods + .transferencia, + child: const Text('- Transferencia'), + ), + ] else + const Text( + 'No hay metodos de pago registrados', + style: TextStyle( + fontSize: 14, + color: Colors.black45, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart new file mode 100644 index 0000000..3610168 --- /dev/null +++ b/lib/ui/views/profile_view.dart @@ -0,0 +1,324 @@ +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:prosapp_web_app/models/city.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/cities_provider.dart'; +import 'package:prosapp_web_app/providers/profile_form_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ProfileView extends StatefulWidget { + const ProfileView({super.key}); + + @override + State createState() => _ProfileViewState(); +} + +class _ProfileViewState extends State { + Usuario? user; + List cities = []; + + @override + void initState() { + super.initState(); + + final authProvider = Provider.of(context, listen: false); + final profileFormProvider = + Provider.of(context, listen: false); + final citiesProvider = Provider.of(context, listen: false); + + profileFormProvider.user = authProvider.user; + setState(() { + cities = citiesProvider.cities; + user = authProvider.user; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < 700) { + return ListView( + physics: const ClampingScrollPhysics(), + children: const [SizedBox(height: 10), _ProfileViewBody()], + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: ListView( + physics: const ClampingScrollPhysics(), + children: const [SizedBox(height: 10), _ProfileViewBody()], + ), + ); + } + }); + } +} + +class _ProfileViewBody extends StatelessWidget { + const _ProfileViewBody(); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < 700) { + return const Column( + children: [ + _AvatarContainer(containerFull: true), + _ProfileViewForm(), + ], + ); + } else { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Table( + columnWidths: const { + 0: FixedColumnWidth(250), + }, + children: const [ + TableRow( + children: [ + _AvatarContainer(containerFull: true), + _ProfileViewForm(), + ], + ) + ], + ), + ), + ); + } + }); + } +} + +class _ProfileViewForm extends StatelessWidget { + const _ProfileViewForm(); + + @override + Widget build(BuildContext context) { + final profileFormProvider = Provider.of(context); + final citiesProvider = Provider.of(context); + final cities = citiesProvider.cities; + final user = profileFormProvider.user!; + + return WhiteCard( + title: 'Información general', + child: Form( + key: profileFormProvider.formKey, + autovalidateMode: AutovalidateMode.always, + child: Column( + children: [ + const SizedBox(height: 10), + TextFormField( + initialValue: user.name, + validator: (value) { + if (value == null || value.isEmpty) { + return 'El nombre es obligatorio'; + } + if (value.trim().length < 4) { + return 'El nombre debe tener al menos 4 caracteres'; + } + return null; + }, + onChanged: (value) { + profileFormProvider.copyUserWith(name: value); + }, + decoration: CustomInputs.formInputDecoration( + hint: 'Nombre de usuario', + label: 'Nombre', + icon: Icons.person_outline, + ), + ), + const SizedBox(height: 20), + TextFormField( + readOnly: true, + onTap: user.phone == null || user.phone!.isEmpty + ? () { + NavigationService.navigateTo(Flurorouter.phoneRoute); + } + : null, + initialValue: user.phone ?? '', + decoration: CustomInputs.formInputDecoration( + hint: 'Número de teléfono', + label: 'Teléfono', + icon: Icons.phone, + ), + ), + const SizedBox(height: 20), + TextFormField( + readOnly: true, + onTap: user.email == null || user.email!.isEmpty + ? () { + NavigationService.navigateTo(Flurorouter.emailRoute); + } + : null, + initialValue: user.email ?? '', + decoration: CustomInputs.formInputDecoration( + hint: 'Correo del usuario', + label: 'Correo', + icon: Icons.email_outlined, + ), + ), + const SizedBox(height: 20), + DropdownButtonFormField( + validator: (value) { + if (value == null) { + return 'La ciudad es obligatoria'; + } + return null; + }, + value: user.city == '' ? null : user.city, + decoration: CustomInputs.formInputDecoration( + hint: 'Selecciona tu ciudad', + label: 'Ciudad', + icon: Icons.location_city_outlined, + ), + items: cities.map((City ciudad) { + return DropdownMenuItem( + value: ciudad.cityName, + child: Text('${ciudad.cityName} - ${ciudad.stateOfCity}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + )), + ); + }).toList(), + onChanged: (value) { + profileFormProvider.copyUserWith(city: value!); + }), + const SizedBox(height: 20), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () async { + await profileFormProvider.updateUserInfo(); + + Provider.of(context, listen: false) + .refreshUser(); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Guardar', + style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ); + } +} + +class _AvatarContainer extends StatelessWidget { + final bool containerFull; + + const _AvatarContainer({required this.containerFull}); + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).user!; + final profileFormProvider = Provider.of(context); + + final image = (profileFormProvider.user!.picture == '' || + profileFormProvider.user!.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: profileFormProvider.user!.picture!, + ); + + return WhiteCard( + width: containerFull ? null : 250, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + user.name, + style: CustomLabels.h2, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + SizedBox( + width: 160, + height: 160, + child: Stack( + children: [ + SizedBox( + width: 200, + height: 200, + child: ClipOval(child: image), + ), + Positioned( + bottom: 5, + right: 5, + child: Container( + width: 45, + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all(color: Colors.white, width: 5), + ), + child: FloatingActionButton( + onPressed: () async { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + withData: true, + ); + + if (result != null) { + PlatformFile file = result.files.first; + Uint8List? fileBytes = file.bytes; + + if (fileBytes != null) { + NotificationsService.showBusyIndicator(context); + await profileFormProvider + .uploadPicture(fileBytes); + Provider.of(context, listen: false) + .refreshUser(); + + Navigator.pop(context); + } + } else {} + }, + backgroundColor: Colors.indigo, + elevation: 0, + child: const Icon( + Icons.camera_alt_outlined, + size: 20, + color: Colors.white, + ), + ), + ), + ) + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } +} diff --git a/lib/ui/views/rating_view.dart b/lib/ui/views/rating_view.dart new file mode 100644 index 0000000..6b95117 --- /dev/null +++ b/lib/ui/views/rating_view.dart @@ -0,0 +1,274 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/models/comment_entity.dart'; +import 'package:prosapp_web_app/providers/score_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; + +class RatingView extends StatefulWidget { + final String type; + final String serviceId; + final String professionalId; + + const RatingView({ + super.key, + required this.type, + required this.serviceId, + required this.professionalId, + }); + + @override + State createState() => _RatingViewState(); +} + +class _RatingViewState extends State { + double _rating = 1.0; + TextEditingController commentController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final servicesProvider = + Provider.of(context, listen: false); + final scoreProvider = Provider.of(context); + + if (widget.type == 'user') { + servicesProvider.getServiceForUser(widget.serviceId); + } + + if (widget.type == 'professional') { + servicesProvider.getServiceForProfessional(widget.serviceId); + } + + return Consumer( + builder: (context, servicesProvider, child) { + if (servicesProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final service = servicesProvider.service!.service; + final user = servicesProvider.service!.user; + + if (widget.type == 'user' && service.professionalScored == true) { + WidgetsBinding.instance.addPostFrameCallback((_) { + navigateTo(Flurorouter.dashboardRoute); + }); + } + + if (widget.type == 'professional' && service.userScored == true) { + WidgetsBinding.instance.addPostFrameCallback((_) { + navigateTo(Flurorouter.dashboardRoute); + }); + } + + final image = (user.picture == '' || user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: user.picture!, + ); + + return ListView( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 100, + height: 100, + child: ClipOval(child: image), + ), + const SizedBox(height: 16.0), + Text( + user.name.toUpperCase(), + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + const Text( + 'Califica el servicio', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 10), + RatingBar.builder( + initialRating: _rating, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 35, + glow: false, + maxRating: 5, + itemPadding: + const EdgeInsets.symmetric(horizontal: 5), + itemBuilder: (context, _) => const Icon( + Icons.star, + color: Color(0xFF2BA4EC), + ), + onRatingUpdate: (rating) { + setState(() { + _rating = rating; + }); + }, + ignoreGestures: false, + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + customMessage(_rating), + style: const TextStyle( + fontSize: 17, + color: Color(0xFF2BA4EC), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 80, vertical: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Comentario', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600), + ), + TextFormField( + decoration: const InputDecoration( + labelStyle: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w600, + ), + ), + maxLines: null, + maxLength: 500, + keyboardType: TextInputType.multiline, + controller: commentController, + ), + ], + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 180), + child: ElevatedButton( + onPressed: () async { + if (widget.type == 'user') { + final newComment = CommentEntity( + content: commentController.text, + score: _rating, + isFromUser: true, + serviceId: widget.serviceId, + createdAt: Timestamp.now(), + authorId: service.userId, + destinationId: service.professionalId, + ); + + await scoreProvider.addComment( + newComment, + ); + + servicesProvider.changeProfessionalScored( + widget.serviceId); + } + + if (widget.type == 'professional') { + final newComment = CommentEntity( + content: commentController.text, + score: _rating, + isFromUser: false, + serviceId: widget.serviceId, + createdAt: Timestamp.now(), + authorId: service.professionalId, + destinationId: service.userId, + ); + + await scoreProvider.addComment( + newComment, + ); + + servicesProvider + .changeUserScored(widget.serviceId); + } + + navigateTo(Flurorouter.dashboardRoute); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all( + Colors.transparent), + ), + child: const Text( + 'Enviar calificación', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + }); + } + + void navigateTo(String routeName) { + NavigationService.replaceTo(routeName); + SideMenuProvider.closeMenu(); + } + + String customMessage(double rating) { + if (rating > 4.0) { + return '¡Excelente! 👏🌟'; + } + + if (rating < 2.0) { + return '¡Malo! 😔❌'; + } + + if (rating <= 4.0 && rating >= 3.0) { + return '¡Bueno! 👍😊'; + } + + if (rating < 3.0 && rating >= 2.0) { + return '¡Regular! 😐🔄'; + } + + return ''; + } +} diff --git a/lib/ui/views/register_view.dart b/lib/ui/views/register_view.dart new file mode 100644 index 0000000..edca397 --- /dev/null +++ b/lib/ui/views/register_view.dart @@ -0,0 +1,135 @@ +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/register_form_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart'; +import 'package:prosapp_web_app/ui/buttons/link_text.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class RegisterView extends StatelessWidget { + const RegisterView({super.key}); + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + + return ChangeNotifierProvider( + create: (_) => RegisterFormProvider(), + child: Builder(builder: (context) { + final registerFormProvider = + Provider.of(context, listen: false); + + return Container( + margin: const EdgeInsets.only(top: 40), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 370), + child: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + key: registerFormProvider.formKey, + child: Column( + children: [ + // Name + TextFormField( + validator: (value) { + if (value == null || + value.isEmpty || + value.trim().isEmpty) { + return 'El nombre es obligatorio'; + } + + if (value.trim().length < 3) { + return 'El nombre debe tener al menos 3 caracteres'; + } + return null; + }, + onChanged: (value) => registerFormProvider.name = value, + decoration: CustomInputs.loginInputDecoration( + hint: 'Ingrese su nombre', + label: 'Nombre', + icon: Icons.person_outline, + ), + ), + const SizedBox(height: 20), + + // Email + TextFormField( + validator: (value) { + if (!EmailValidator.validate(value ?? '')) { + return 'Email no válido'; + } + return null; + }, + onChanged: (value) => registerFormProvider.email = value, + decoration: CustomInputs.loginInputDecoration( + hint: 'Ingresa tu correo', + label: 'Email', + icon: Icons.email_outlined, + ), + ), + const SizedBox(height: 20), + + // Password + TextFormField( + validator: (value) { + if (value == null || value.length < 8) { + return "La contraseña debe tener al menos 8 caracteres"; + } + return null; + }, + onChanged: (value) => + registerFormProvider.password = value, + obscureText: true, + decoration: CustomInputs.loginInputDecoration( + hint: 'Ingresa tu contraseña', + label: 'Contraseña', + icon: Icons.lock_outline, + ), + ), + const SizedBox(height: 20), + + CustomOutlinedButton( + onPressed: () async { + final validForm = registerFormProvider.validateForm(); + + if (!validForm) return; + + await authProvider.register( + registerFormProvider.email, + registerFormProvider.password, + registerFormProvider.name, + ); + }, + text: "Crear cuenta", + color: Colors.blue, + ), + const SizedBox(height: 20), + + LinkText( + text: "Iniciar sesión", + onPressed: () { + Navigator.pushReplacementNamed( + context, Flurorouter.loginRoute); + }, + ), + const SizedBox(height: 5), + LinkText( + text: "Entrar con celular", + onPressed: () { + Navigator.pushReplacementNamed( + context, Flurorouter.phoneLoginRoute); + }, + ), + ], + ), + ), + ), + ), + ); + }), + ); + } +} diff --git a/lib/ui/views/request_professional_view.dart b/lib/ui/views/request_professional_view.dart new file mode 100644 index 0000000..7752f74 --- /dev/null +++ b/lib/ui/views/request_professional_view.dart @@ -0,0 +1,492 @@ +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:prosapp_web_app/models/pro_state.dart'; +import 'package:prosapp_web_app/models/profession.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/professional_form_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/professions_provider.dart'; +import 'package:prosapp_web_app/providers/profile_form_provider.dart'; +import 'package:prosapp_web_app/services/notifications_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/ui/views/no_page_found_view.dart'; +import 'package:provider/provider.dart'; + +class RequestProfessionalView extends StatefulWidget { + const RequestProfessionalView({super.key}); + + @override + State createState() => + _RequestProfessionalViewState(); +} + +class _RequestProfessionalViewState extends State { + Usuario? user; + List professions = []; + late ProfessionalFormProvider professionalFormProvider; + + @override + void initState() { + super.initState(); + + final authProvider = Provider.of(context, listen: false); + final profileFormProvider = + Provider.of(context, listen: false); + professionalFormProvider = + Provider.of(context, listen: false); + final professionsProvider = + Provider.of(context, listen: false); + final proProvider = + Provider.of(context, listen: false); + + profileFormProvider.user = authProvider.user; + + proProvider.getProfessional(authProvider.user!.id).then((value) { + professionalFormProvider.setProfesional(value); + }); + + setState(() { + professions = professionsProvider.professions; + user = authProvider.user; + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth < 900) { + return ListView( + physics: const ClampingScrollPhysics(), + children: const [SizedBox(height: 10), _ProfileViewForm()], + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: ListView( + physics: const ClampingScrollPhysics(), + children: const [SizedBox(height: 10), _ProfileViewForm()], + ), + ); + } + }); + } +} + +class _ProfileViewForm extends StatefulWidget { + const _ProfileViewForm(); + + @override + State<_ProfileViewForm> createState() => _ProfileViewFormState(); +} + +class _ProfileViewFormState extends State<_ProfileViewForm> { + final TextEditingController _specialityController = TextEditingController(); + + List specializations = []; + + void _addItemToList() { + setState(() { + String newItem = _specialityController.text.trim(); + if (newItem.isNotEmpty) { + specializations.add(newItem); + _specialityController.clear(); + } + }); + } + + void _removeItemFromList(String item) { + setState(() { + specializations.remove(item); + }); + } + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + final professionsProvider = Provider.of(context); + final professions = professionsProvider.professions; + final user = authProvider.user!; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Consumer( + builder: (context, professionalFormProvider, child) { + if (professionalFormProvider.profesional == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final professional = professionalFormProvider.profesional; + + switch (enumToInt(user.proState)) { + case 0: + return WhiteCard( + title: 'Información profesional', + child: Form( + key: professionalFormProvider.formKey, + autovalidateMode: AutovalidateMode.always, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const SizedBox(height: 10), + TextFormField( + initialValue: professional!.identification, + validator: (value) { + if (value == null || value.isEmpty) { + return 'La cedula es obligatoria'; + } + if (value.trim().length < 6) { + return 'La cedula debe tener al menos 6 caracteres'; + } + return null; + }, + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + identification: value); + }, + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa tu cedula', + label: 'Cedula', + icon: Icons.badge_outlined, + ), + ), + const SizedBox(height: 10), + ElevatedButton.icon( + onPressed: () async { + try { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf'], + withData: true, + ); + + if (result != null) { + PlatformFile file = result.files.first; + Uint8List? fileBytes = file.bytes; + + if (fileBytes != null) { + NotificationsService.showBusyIndicator(context); + + final provider = + Provider.of( + context, + listen: false); + await provider.uploadPdfIdentification( + fileBytes, user.id); + + Navigator.pop(context); + } + } + } catch (e) { + print('debugeando $e'); + } + }, + icon: const Icon(Icons.upload_file), + label: const Text('Cargar pdf de la cedula'), + ), + const SizedBox(height: 20), + DropdownButtonFormField( + validator: (value) { + if (value == null) { + return 'La profesión es obligatoria'; + } + return null; + }, + value: professional.profession == '' + ? null + : professional.profession, + decoration: CustomInputs.formInputDecoration( + hint: 'Selecciona tu profesión', + label: 'Profesión', + icon: Icons.work_outline_outlined, + ), + items: professions.map((Profession profession) { + return DropdownMenuItem( + value: profession.name, + child: Text( + profession.name, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ); + }).toList(), + onChanged: (value) { + professionalFormProvider.copyProfesionalWith( + profession: value); + }), + const SizedBox(height: 10), + ElevatedButton.icon( + onPressed: () async { + try { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf'], + withData: true, + ); + + if (result != null) { + PlatformFile file = result.files.first; + Uint8List? fileBytes = file.bytes; + + if (fileBytes != null) { + NotificationsService.showBusyIndicator(context); + + final provider = + Provider.of( + context, + listen: false); + await provider.uploadPdfCertificate( + fileBytes, user.id); + + Navigator.pop(context); + } + } + } catch (e) { + print('debugeando $e'); + } + }, + icon: const Icon(Icons.upload_file), + label: const Text('Cargar pdf del certificado'), + ), + const SizedBox(height: 20), + TextFormField( + validator: (value) { + if (RegExp(r'\s{2,}').hasMatch(value!)) { + return 'La especialización no es valida'; + } + return null; + }, + onFieldSubmitted: (_) { + _addItemToList(); + }, + controller: _specialityController, + decoration: CustomInputs.formInputDecoration( + hint: 'Ingresa tus especializaciones y agregalas (+)', + label: 'Especializaciones', + icon: Icons.assignment_outlined, + iconButton: IconButton( + onPressed: () { + _addItemToList(); + }, + icon: const Icon(Icons.add), + ), + ), + ), + const SizedBox(height: 10), + ElevatedButton.icon( + onPressed: () async { + try { + FilePickerResult? result = + await FilePicker.platform.pickFiles( + type: FileType.custom, + allowMultiple: true, + allowedExtensions: ['pdf'], + withData: true, + ); + + if (result != null) { + List filesBytes = result.files + .where((file) => file.bytes != null) + .map((file) => file.bytes!) + .toList(); + + if (filesBytes.isNotEmpty) { + NotificationsService.showBusyIndicator(context); + + final provider = + Provider.of( + context, + listen: false); + await provider.uploadPdfSpecializations( + filesBytes, user.id); + + Navigator.pop(context); + } + } + } catch (e) { + print('debugeando $e'); + } + }, + icon: const Icon(Icons.upload_file), + label: + const Text('Cargar pdfs de las especializaciones'), + ), + const SizedBox(height: 10), + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: specializations + .map((item) => Chip( + label: Text(item), + backgroundColor: Colors.blue.withOpacity(0.3), + labelStyle: + const TextStyle(color: Colors.blue), + deleteIconColor: Colors.blue, + onDeleted: () { + _removeItemFromList(item); + }, + shape: RoundedRectangleBorder( + side: const BorderSide( + color: Colors.blue, width: 0.3), + borderRadius: BorderRadius.circular(8), + ), + )) + .toList(), + ), + const SizedBox(height: 20), + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 180), + child: ElevatedButton( + onPressed: () async { + professionalFormProvider.copyProfesionalWith( + specializations: specializations); + final res = await professionalFormProvider + .updateProfesionalInfo(user.id); + + if (res) { + Provider.of(context, + listen: false) + .refreshUser(); + + final profileFormProvider = + Provider.of(context, + listen: false); + profileFormProvider.copyUserWith( + proState: ProState.pending); + profileFormProvider.updateUserInfoNoValid(); + } + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5)), + ), + ), + shadowColor: + WidgetStateProperty.all(Colors.transparent), + ), + child: const Text( + 'Enviar a revisión', + style: TextStyle( + color: Colors.white, + fontSize: 15, + ), + ), + ), + ), + ), + const SizedBox(height: 15), + ], + ), + ), + ); + case 1: + return LayoutBuilder( + builder: (context, constraints) { + double screenWidth = constraints.maxWidth; + double baseFontSize = 18; + double responsiveFontSize = + screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize; + + return WhiteCard( + child: Column( + children: [ + const Center( + child: Image( + image: AssetImage('checklist.gif'), + width: 320, + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1020), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'Gracias por proporcionar tu información. Actualmente, estamos revisando tus datos y una vez aprobados, podrás acceder al perfil profesional sin problemas. Te notificaremos tan pronto como tu cuenta esté lista.', + style: TextStyle(fontSize: responsiveFontSize), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(height: 20), + Text( + '¡Gracias por tu paciencia!', + style: TextStyle(fontSize: responsiveFontSize), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + ], + ), + ); + }, + ); + + case 3: + return LayoutBuilder( + builder: (context, constraints) { + double screenWidth = constraints.maxWidth; + double baseFontSize = 18; + double responsiveFontSize = + screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize; + + return WhiteCard( + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 25), + child: const Center( + child: Icon( + Icons.sentiment_dissatisfied_outlined, + size: 100, + color: Colors.red, + ), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1020), + child: Container( + margin: const EdgeInsets.only( + left: 8, right: 8, bottom: 10), + child: Text( + 'Lamentablemente, tu solicitud no ha sido aceptada en esta ocasión. Por favor, revisa tus datos y vuelve a intentarlo más tarde.', + style: TextStyle(fontSize: responsiveFontSize), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(height: 20), + Text( + '¡Gracias por tu paciencia!', + style: TextStyle(fontSize: responsiveFontSize), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + ], + ), + ); + }, + ); + } + + return Center(child: Text('No se encontró la sección.')); + // return const NoPageFoundView(); + }), + ), + ); + } +} diff --git a/lib/ui/views/schedule_view.dart b/lib/ui/views/schedule_view.dart new file mode 100644 index 0000000..0c78603 --- /dev/null +++ b/lib/ui/views/schedule_view.dart @@ -0,0 +1,577 @@ +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/professional_form_provider.dart'; +import 'package:prosapp_web_app/providers/professional_provider.dart'; +import 'package:prosapp_web_app/providers/profile_form_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/ui/shared/widgets/schedule_day_tile.dart'; +import 'package:provider/provider.dart'; + +class ScheduleView extends StatefulWidget { + const ScheduleView({super.key}); + + @override + State createState() => _ScheduleViewState(); +} + +class _ScheduleViewState extends State { + Usuario? user; + late ProfessionalFormProvider professionalFormProvider; + + @override + void initState() { + super.initState(); + + final authProvider = Provider.of(context, listen: false); + final profileFormProvider = + Provider.of(context, listen: false); + professionalFormProvider = + Provider.of(context, listen: false); + + final proProvider = + Provider.of(context, listen: false); + + profileFormProvider.user = authProvider.user; + + proProvider.getProfessional(authProvider.user!.id).then((value) { + professionalFormProvider.setProfesional(value); + }); + + profileFormProvider.user = authProvider.user; + setState(() { + user = authProvider.user; + }); + } + + bool isTimeRangeValid(TimeOfDay startTime, TimeOfDay endTime) { + final start = DateTime(2024, 1, 1, startTime.hour, startTime.minute); + final end = DateTime(2024, 1, 1, endTime.hour, endTime.minute); + return start.isBefore(end); + } + + @override + Widget build(BuildContext context) { + final profileFormProvider = Provider.of(context); + final user = profileFormProvider.user!; + + return Consumer( + builder: (context, professionalFormProvider, child) { + if (professionalFormProvider.profesional == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + final profesional = professionalFormProvider.profesional!; + + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + title: 'Horario', + child: Column( + children: [ + ScheduleDayTile( + day: 'Lunes', + schedule: profesional.schedules.monday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + monday: profesional.schedules.monday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + monday: profesional.schedules.monday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + monday: profesional.schedules.monday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + monday: profesional.schedules.monday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + monday: profesional.schedules.monday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + monday: profesional.schedules.monday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + ScheduleDayTile( + day: 'Martes', + schedule: profesional.schedules.tuesday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + tuesday: profesional.schedules.tuesday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + tuesday: profesional.schedules.tuesday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + tuesday: profesional.schedules.tuesday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + tuesday: profesional.schedules.tuesday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + tuesday: profesional.schedules.tuesday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + tuesday: profesional.schedules.tuesday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + ScheduleDayTile( + day: 'Miercoles', + schedule: profesional.schedules.wednesday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + wednesday: + profesional.schedules.wednesday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + wednesday: + profesional.schedules.wednesday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + wednesday: profesional.schedules.wednesday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + wednesday: profesional.schedules.wednesday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + wednesday: profesional.schedules.wednesday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + wednesday: profesional.schedules.wednesday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + ScheduleDayTile( + day: 'Jueves', + schedule: profesional.schedules.thursday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + thursday: profesional.schedules.thursday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + thursday: profesional.schedules.thursday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + thursday: profesional.schedules.thursday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + thursday: profesional.schedules.thursday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + thursday: profesional.schedules.thursday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + thursday: profesional.schedules.thursday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + ScheduleDayTile( + day: 'Viernes', + schedule: profesional.schedules.friday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + friday: profesional.schedules.friday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + friday: profesional.schedules.friday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + friday: profesional.schedules.friday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + friday: profesional.schedules.friday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + friday: profesional.schedules.friday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + friday: profesional.schedules.friday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + ScheduleDayTile( + day: 'Sabado', + schedule: profesional.schedules.saturday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + saturday: profesional.schedules.saturday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + saturday: profesional.schedules.saturday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + saturday: profesional.schedules.saturday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + saturday: profesional.schedules.saturday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + saturday: profesional.schedules.saturday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + saturday: profesional.schedules.saturday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + ScheduleDayTile( + day: 'Domingo', + schedule: profesional.schedules.sunday, + onEnableChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + sunday: profesional.schedules.sunday.copyWith( + enabled: value, + ), + ), + ); + }); + }, + onContinuousDayChanged: (value) { + setState(() { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + sunday: profesional.schedules.sunday.copyWith( + continuousDay: value, + ), + ), + ); + }); + }, + onRange1Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + sunday: profesional.schedules.sunday.copyWith( + range1Hour1: pickedTime, + ), + ), + ); + }, + onRange1Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + sunday: profesional.schedules.sunday.copyWith( + range1Hour2: pickedTime, + ), + ), + ); + }, + onRange2Hour1Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + sunday: profesional.schedules.sunday.copyWith( + range2Hour1: pickedTime, + ), + ), + ); + }, + onRange2Hour2Pick: (pickedTime) { + professionalFormProvider.copyProfesionalWith( + schedules: profesional.schedules.copyWith( + sunday: profesional.schedules.sunday.copyWith( + range2Hour2: pickedTime, + ), + ), + ); + }, + ), + const Divider( + height: 20, + ), + Container( + margin: const EdgeInsets.only(top: 10), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () async { + + + await professionalFormProvider.updateProfesionalProfileScheduleInfo(user.id); + + NavigationService.replaceTo(Flurorouter.professionalProfileRoute); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5)), + )), + shadowColor: + WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Guardar', + style: TextStyle(color: Colors.white)), + ), + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ), + ], + ); + }); + } +} diff --git a/lib/ui/views/service_view.dart b/lib/ui/views/service_view.dart new file mode 100644 index 0000000..2a5d533 --- /dev/null +++ b/lib/ui/views/service_view.dart @@ -0,0 +1,967 @@ +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/service_location_preferences.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; +import 'package:prosapp_web_app/models/usuario.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; +import 'package:prosapp_web_app/providers/settings_provider.dart'; +import 'package:prosapp_web_app/providers/sidemenu_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/ui/views/no_page_found_view.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:prosapp_web_app/utils/local_notifications.dart'; + +class ServiceView extends StatelessWidget { + final String type; + final String serviceId; + + const ServiceView({ + super.key, + required this.type, + required this.serviceId, + }); + + @override + Widget build(BuildContext context) { + final settingsProvider = Provider.of(context); + final servicesProvider = + Provider.of(context, listen: false); + + if (type == 'user') { + servicesProvider.getServiceForUser(serviceId); + } + + if (type == 'professional') { + servicesProvider.getServiceForProfessional(serviceId); + } + + return Consumer( + builder: (context, servicesProvider, child) { + if (servicesProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (servicesProvider.service == null) { + // return const NoPageFoundView(); + return Center(child: Text('No se encontró el servicio.')); + } + + final service = servicesProvider.service!.service; + final user = servicesProvider.service!.user; + + final image = (user.picture == '' || user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: user.picture!, + ); + + return ListView( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: WhiteCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 100, + height: 100, + child: ClipOval(child: image), + ), + const SizedBox(height: 16.0), + Text( + user.name.toUpperCase(), + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5.0), + Text( + '${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(service.day))} - ${ScheduleEntity.getFormatTime(service.range1Hour1)}', + style: const TextStyle( + fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 10.0), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 5, + ), + decoration: BoxDecoration( + color: const Color(0xFFD6F4FF), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(1, 3), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 20, + color: Colors.black54, + ), + const SizedBox(width: 10), + Text( + _CustomServiceLocation(service.location), + style: const TextStyle( + fontSize: 16, color: Colors.black54), + ), + ], + ), + ), + if (service.description.isNotEmpty) ...[ + const SizedBox(height: 12.0), + Text( + '"${service.description.trim()}"', + style: const TextStyle(color: Colors.grey), + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: 20.0), + customActionButtons(service, user), + const SizedBox(height: 40.0), + customMessageStatus(service, user), + const SizedBox(height: 20.0), + customButton( + service, context, user, servicesProvider), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + }); + } + + void navigateTo(String routeName) { + NavigationService.replaceTo(routeName); + SideMenuProvider.closeMenu(); + } + + Widget customActionButtons(Service service, Usuario user) { + DateTime serviceDate = DateTime.parse(service.day); + DateTime now = DateTime.now(); + DateTime serviceDateTime = DateTime( + serviceDate.year, + serviceDate.month, + serviceDate.day, + service.range1Hour2.hour, + service.range1Hour2.minute, + ); + + if (service.status == ServiceStatus.acepted || + service.status == ServiceStatus.active) { + if (now.isAfter(serviceDateTime)) { + return const SizedBox(); + } else { + return Wrap( + alignment: WrapAlignment.center, + spacing: 15, + children: [ + Visibility( + visible: user.phone == null || user.phone == '' ? false : true, + child: ElevatedButton( + onPressed: () => launch("tel:${user.phone}"), + style: ElevatedButton.styleFrom( + foregroundColor: const Color(0xFF2BA4EC), + backgroundColor: Colors.white, + shape: const CircleBorder( + side: BorderSide( + color: Color(0xFF2BA4EC), + width: 2, + ), + ), + ), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0), + child: Icon( + Icons.phone_android, + size: 30, + color: Color(0xFF2BA4EC), + ), + ), + ), + ), + ElevatedButton( + onPressed: () { + if (type == 'user') { + NavigationService.navigateTo( + '/dashboard/user/service/${service.id}/chat/${user.id}'); + } + + if (type == 'professional') { + NavigationService.navigateTo( + '/dashboard/professional/service/${service.id}/chat/${user.id}'); + } + }, + style: ElevatedButton.styleFrom( + foregroundColor: const Color(0xFF2BA4EC), + backgroundColor: Colors.white, + shape: const CircleBorder( + side: BorderSide( + color: Color(0xFF2BA4EC), + width: 2, + ), + ), + ), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0), + child: Icon( + Icons.message, + size: 30, + color: Color(0xFF2BA4EC), + ), + ), + ), + ], + ); + } + } + + return const SizedBox(); + } + + Widget customMessageStatus(Service service, Usuario user) { + DateTime serviceDate = DateTime.parse(service.day); + DateTime now = DateTime.now(); + DateTime serviceDateTime = DateTime( + serviceDate.year, + serviceDate.month, + serviceDate.day, + service.range1Hour2.hour, + service.range1Hour2.minute, + ); + + if (type == 'user') { + if (service.status == ServiceStatus.completed && + service.professionalScored == false) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.green.shade50, + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Column( + children: [ + const Text( + 'Completado', + style: TextStyle(fontSize: 32, color: Colors.green), + ), + const Text( + 'Califica el servicio', + style: TextStyle(fontSize: 20, color: Colors.green), + ), + const SizedBox(height: 15), + FilledButton( + onPressed: () { + NavigationService.navigateTo( + '/dashboard/user/service/${service.id}/rating/${user.id}'); + }, + style: FilledButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Container( + alignment: Alignment.center, + child: const Text( + 'Calificar', + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + ), + ], + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.green.shade50, width: 4), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.star, + color: Colors.green, + size: 48, + ), + ), + ) + ], + ); + } + + if (service.status == ServiceStatus.completed && + service.professionalScored == true) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.green.shade50, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Column( + children: [ + Text( + 'Completado', + style: TextStyle(fontSize: 32, color: Colors.green), + ), + ], + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.green.shade50, width: 4), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.star, + color: Colors.green, + size: 48, + ), + ), + ) + ], + ); + } + + if (service.status == ServiceStatus.cancelled) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.red.shade100, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Text( + 'Cancelado', + style: TextStyle(fontSize: 32, color: Colors.red), + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red.shade100, width: 4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close_rounded, + color: Colors.red, size: 48), + ), + ) + ], + ); + } + + if (service.status == ServiceStatus.denied) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.red.shade100, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Text( + 'Rechazado', + style: TextStyle(fontSize: 32, color: Colors.red), + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red.shade100, width: 4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close_rounded, + color: Colors.red, size: 48), + ), + ) + ], + ); + } + + if (now.isAfter(serviceDateTime)) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.red.shade100, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Caducado', + style: TextStyle(fontSize: 32, color: Colors.red), + ), + Text( + 'Tu servicio ha excedido \n el tiempo de espera', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, color: Colors.red), + ), + ], + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red.shade100, width: 4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close_rounded, + color: Colors.red, size: 48), + ), + ) + ], + ); + } + } + + if (type == 'professional') { + if (service.status == ServiceStatus.completed && + service.userScored == false) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.green.shade50, + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Column( + children: [ + const Text( + 'Completado', + style: TextStyle(fontSize: 32, color: Colors.green), + ), + const Text( + 'Califica el servicio', + style: TextStyle(fontSize: 20, color: Colors.green), + ), + const SizedBox(height: 15), + FilledButton( + onPressed: () { + NavigationService.navigateTo( + '/dashboard/professional/service/${service.id}/rating/${user.id}'); + }, + style: FilledButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Container( + alignment: Alignment.center, + child: const Text( + 'Calificar', + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + ), + ], + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.green.shade50, width: 4), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.star, + color: Colors.green, + size: 48, + ), + ), + ) + ], + ); + } + + if (service.status == ServiceStatus.completed && + service.userScored == true) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.green.shade50, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Column( + children: [ + Text( + 'Completado', + style: TextStyle(fontSize: 32, color: Colors.green), + ), + ], + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.green.shade50, width: 4), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.star, + color: Colors.green, + size: 48, + ), + ), + ) + ], + ); + } + + if (service.status == ServiceStatus.cancelled) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.red.shade100, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Text( + 'Cancelado', + style: TextStyle(fontSize: 32, color: Colors.red), + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red.shade100, width: 4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close_rounded, + color: Colors.red, size: 48), + ), + ) + ], + ); + } + if (service.status == ServiceStatus.denied) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.red.shade100, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Text( + 'Rechazado', + style: TextStyle(fontSize: 32, color: Colors.red), + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red.shade100, width: 4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close_rounded, + color: Colors.red, size: 48), + ), + ) + ], + ); + } + + if (now.isAfter(serviceDateTime)) { + return Stack( + alignment: AlignmentDirectional.topCenter, + clipBehavior: Clip.none, + children: [ + Card( + color: Colors.red.shade100, + child: const Padding( + padding: EdgeInsets.fromLTRB(32, 56, 32, 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Caducado', + style: TextStyle(fontSize: 32, color: Colors.red), + ), + Text( + 'Tu servicio ha excedido \n el tiempo de espera', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, color: Colors.red), + ), + ], + ), + ), + ), + Positioned( + top: -40, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red.shade100, width: 4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close_rounded, + color: Colors.red, size: 48), + ), + ) + ], + ); + } + } + + return const SizedBox(); + } + + Widget customButton(Service service, BuildContext context, Usuario userInfo, + ServicesProvider servicesProvider) { + DateTime serviceDate = DateTime.parse(service.day); + DateTime now = DateTime.now(); + DateTime serviceDateTime = DateTime( + serviceDate.year, + serviceDate.month, + serviceDate.day, + service.range1Hour2.hour, + service.range1Hour2.minute, + ); + + if (now.isAfter(serviceDateTime)) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () { + navigateTo(Flurorouter.dashboardRoute); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Volver', style: TextStyle(color: Colors.white)), + ), + ); + } else { + if (type == 'professional') { + if (service.status == ServiceStatus.pending) { + return Column( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 160), + child: ElevatedButton( + onPressed: () async { + await servicesProvider.changeServiceStatus( + service.id!, ServiceStatus.denied); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.red.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Rechazar servicio', + style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(height: 8), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 160), + child: ElevatedButton( + onPressed: () { + servicesProvider.changeServiceStatus( + service.id!, ServiceStatus.acepted); + + navigateTo(Flurorouter.dashboardRoute); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Aceptar servicio', + style: TextStyle(color: Colors.white)), + ), + ), + ], + ); + } + + if (service.status == ServiceStatus.acepted) { + if (serviceDateTime.difference(now).inDays > 1) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () { + servicesProvider.changeServiceStatus( + service.id!, ServiceStatus.denied); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.red.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Cancelar servicio', + style: TextStyle(color: Colors.white)), + ), + ); + } else { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 160), + child: ElevatedButton( + onPressed: () { + servicesProvider.changeServiceStatus( + service.id!, ServiceStatus.active); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Iniciar servicio', + style: TextStyle(color: Colors.white)), + ), + ); + } + } + + if (service.status == ServiceStatus.active) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 160), + child: ElevatedButton( + onPressed: () { + servicesProvider.changeServiceStatus( + service.id!, ServiceStatus.completed); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Terminar servicio', + style: TextStyle(color: Colors.white)), + ), + ); + } + } + + if (type == 'user') { + if (service.status == ServiceStatus.pending) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.red.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Cancelar servicio', + style: TextStyle(color: Colors.white)), + ), + ); + } + if (service.status == ServiceStatus.acepted) { + if (serviceDateTime.difference(now).inDays > 1) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.red.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Cancelar servicio', + style: TextStyle(color: Colors.white)), + ), + ); + } + } + + if (service.status == ServiceStatus.active) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Terminar servicio', + style: TextStyle(color: Colors.white)), + ), + ); + + // GeneralSecondaryButton( + // label: 'Terminar servicio', + // onPressed: () { + // // final currentState = context.read().state; + // // if (currentState is ServiceLoaded) { + // // context.read().add(UpdateServiceStatus( + // // widget.serviceId, ServiceStatus.completed)); + // // } + // }); + } + } + } + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: ElevatedButton( + onPressed: () { + if (type == 'professional') { + navigateTo(Flurorouter.professionalServicesRequestsRoute); + } + if (type == 'user') { + navigateTo(Flurorouter.dashboardRoute); + } + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.blue.shade400, + ), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + )), + shadowColor: WidgetStateProperty.all(Colors.transparent), + ), + child: const Text('Volver', style: TextStyle(color: Colors.white)), + ), + ); + } + + String _CustomServiceLocation(ServiceLocationPreferences location) { + if (type == 'user') { + if (location == ServiceLocationPreferences.office) { + return 'Servicio en sitio / consultorio'; + } + } + + if (type == 'professional') { + if (location == ServiceLocationPreferences.office) { + return 'Servicio en tu consultorio'; + } + } + if (location == ServiceLocationPreferences.delivery) { + return 'Servicio a domicilio'; + } + + return 'Error al cargar el servicio.'; + } +} diff --git a/lib/ui/views/services_history_view.dart b/lib/ui/views/services_history_view.dart new file mode 100644 index 0000000..290dc38 --- /dev/null +++ b/lib/ui/views/services_history_view.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart'; + +import 'package:provider/provider.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; + +import 'package:prosapp_web_app/ui/cards/white_card.dart'; + +class ServicesHistoryView extends StatelessWidget { + final String type; + + const ServicesHistoryView({super.key, required this.type}); + + @override + Widget build(BuildContext context) { + final servicesProvider = + Provider.of(context, listen: false); + + if (type == 'user') { + servicesProvider.getServicesHistoryForUser( + Provider.of(context, listen: false).user!.id); + } + if (type == 'professional') { + servicesProvider.getServicesHistoryForProfessional( + Provider.of(context, listen: false).user!.id); + } + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Consumer( + builder: (context, servicesProvider, child) { + if (servicesProvider.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (servicesProvider.services.isEmpty) { + return ListView( + children: const [ + WhiteCard( + child: Center(child: Text('No hay servicios disponibles.')), + ), + ], + ); + } + + return ListView.builder( + itemCount: servicesProvider.services.length, + itemBuilder: (context, index) { + final data = servicesProvider.services[index]; + + final image = + (data.user.picture == '' || data.user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: data.user.picture!, + ); + + return Container( + margin: const EdgeInsets.only(bottom: 10), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + NavigationService.replaceTo( + '/dashboard/$type/service/${data.service.id}'); + }, + child: WhiteCard( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: SizedBox( + width: 80, + height: 80, + child: ClipOval( + child: image, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + data.user.name, + style: CustomLabels.h2, + ), + if (data.service.description != '') + Text( + '"${data.service.description}"', + style: CustomLabels.h5, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}', + style: const TextStyle( + color: Colors.black54, fontSize: 16), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10), + child: customStatus(data.service), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ), + ), + ); + } +} + +Widget customStatus(Service service) { + if (service.status == ServiceStatus.completed) { + return const StatusItem(text: 'Completado', color: Colors.blueAccent); + } + if (service.status == ServiceStatus.cancelled) { + return const StatusItem(text: 'Cancelado', color: Colors.red); + } + if (service.status == ServiceStatus.denied) { + return const StatusItem(text: 'Rechazado', color: Colors.red); + } + + return const SizedBox(); +} diff --git a/lib/ui/views/services_requests_view.dart b/lib/ui/views/services_requests_view.dart new file mode 100644 index 0000000..c29193c --- /dev/null +++ b/lib/ui/views/services_requests_view.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart'; + +import 'package:provider/provider.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; + +import 'package:prosapp_web_app/ui/cards/white_card.dart'; + +class ServicesRequestsView extends StatelessWidget { + const ServicesRequestsView({super.key}); + + @override + Widget build(BuildContext context) { + final servicesProvider = + Provider.of(context, listen: false); + + servicesProvider.getServicesRequestsForProfessional( + Provider.of(context, listen: false).user!.id); + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Consumer( + builder: (context, servicesProvider, child) { + if (servicesProvider.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (servicesProvider.services.isEmpty) { + return ListView( + children: const [ + WhiteCard( + child: Center(child: Text('No hay servicios disponibles.')), + ), + ], + ); + } + + return ListView.builder( + itemCount: servicesProvider.services.length, + itemBuilder: (context, index) { + final data = servicesProvider.services[index]; + + final image = + (data.user.picture == '' || data.user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: data.user.picture!, + ); + + return Container( + margin: const EdgeInsets.only(bottom: 10), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + NavigationService.replaceTo( + '/dashboard/professional/service/${data.service.id}'); + }, + child: WhiteCard( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: SizedBox( + width: 80, + height: 80, + child: ClipOval( + child: image, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + data.user.name, + style: CustomLabels.h2, + ), + if (data.service.description != '') + Text( + '"${data.service.description}"', + style: CustomLabels.h5, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}', + style: const TextStyle( + color: Colors.black54, fontSize: 16), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10), + child: customStatus(data.service), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ), + ), + ); + } +} + +Widget customStatus(Service service) { + if (service.status == ServiceStatus.pending) { + return const StatusItem(text: 'Solicitud', color: Colors.black45); + } + return const SizedBox(); +} diff --git a/lib/ui/views/services_view.dart b/lib/ui/views/services_view.dart new file mode 100644 index 0000000..3ee0f45 --- /dev/null +++ b/lib/ui/views/services_view.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:prosapp_web_app/models/schedules_entity.dart'; +import 'package:prosapp_web_app/models/service.dart'; +import 'package:prosapp_web_app/models/service_status.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart'; + +import 'package:provider/provider.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/providers/services_provider.dart'; + +import 'package:prosapp_web_app/ui/cards/white_card.dart'; + +class ServicesView extends StatelessWidget { + final String type; + + const ServicesView({super.key, required this.type}); + + @override + Widget build(BuildContext context) { + final servicesProvider = + Provider.of(context, listen: false); + + if (type == 'user') { + servicesProvider.getServicesForUser( + Provider.of(context, listen: false).user!.id); + } + if (type == 'professional') { + servicesProvider.getServicesForProfessional( + Provider.of(context, listen: false).user!.id); + } + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Consumer( + builder: (context, servicesProvider, child) { + if (servicesProvider.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (servicesProvider.services.isEmpty) { + return ListView( + children: const [ + WhiteCard( + child: Center(child: Text('No hay servicios disponibles.')), + ), + ], + ); + } + + return ListView.builder( + itemCount: servicesProvider.services.length, + itemBuilder: (context, index) { + final data = servicesProvider.services[index]; + + final image = + (data.user.picture == '' || data.user.picture == null) + ? const Image(image: AssetImage('no-image.jpg')) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', + fit: BoxFit.cover, + image: data.user.picture!, + ); + + return Container( + margin: const EdgeInsets.only(bottom: 10), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + NavigationService.replaceTo( + '/dashboard/$type/service/${data.service.id}'); + }, + child: WhiteCard( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: SizedBox( + width: 80, + height: 80, + child: ClipOval( + child: image, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + data.user.name, + style: CustomLabels.h2, + ), + if (data.service.description != '') + Text( + '"${data.service.description}"', + style: CustomLabels.h5, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}', + style: const TextStyle( + color: Colors.black54, fontSize: 16), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10), + child: customStatus(data.service), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ), + ), + ); + } +} + +Widget customStatus(Service service) { + if (service.status == ServiceStatus.pending) { + return const StatusItem(text: 'Pendiente', color: Colors.black54); + } + if (service.status == ServiceStatus.acepted) { + return const StatusItem(text: 'Aceptado', color: Colors.green); + } + if (service.status == ServiceStatus.active) { + return const StatusItem(text: 'Activo', color: Colors.blueAccent); + } + + return const SizedBox(); +} diff --git a/lib/ui/views/support_view.dart b/lib/ui/views/support_view.dart new file mode 100644 index 0000000..f8affa8 --- /dev/null +++ b/lib/ui/views/support_view.dart @@ -0,0 +1,211 @@ +import 'package:prosapp_web_app/providers/settings_provider.dart'; +import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SupportView extends StatelessWidget { + const SupportView({super.key}); + + Future _launchURL(String url) async { + final Uri _url = Uri.parse(url); + + if (await canLaunchUrl(_url)) { + await launchUrl( + _url, + mode: LaunchMode.externalApplication, + webOnlyWindowName: '_blank', // Para abrir en una nueva pestaña en web + ); + } else { + throw 'No se pudo abrir la URL $url'; + } + } + + Future _goWhatssApp(String number) async { + final url = 'https://api.whatsapp.com/send?phone=$number&text=Hola%21+soy+usuario+de+Prosapp+y+quisiera+conocer+mas+sobre+esta+app+%F0%9F%98%81'; + await _launchURL(url); + } + + Future _goEmail(String email) async { + final url = 'mailto:$email?subject=${Uri.encodeComponent(email)}'; + + await _launchURL(url); + } + + Future _goSugerencias() async { + const url = 'https://admin.prosapp.co/sugerencias'; + await _launchURL(url); + } + + Future _goPoliticas(String urlText) async { + final url = urlText; + await _launchURL(url); + } + + Future _goTerminos(String urlText) async { + final url = urlText; + await _launchURL(url); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Consumer( + builder: (context, settingsProvider, child) { + if (settingsProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final settings = settingsProvider.settings!; + + return ListView( + physics: const ClampingScrollPhysics(), + children: [ + const SizedBox(height: 10), + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: WhiteCard( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 30), + child: Center( + child: Text( + settings.tituloSoporte, + style: TextStyle( + fontSize: size.width > 500 ? 30 : 20, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ), + ), + Padding( + padding: EdgeInsets.only( + left: size.width > 500 ? 30 : 0, + right: size.width > 500 ? 30 : 0, + bottom: 30, + ), + child: Text( + settings.parrafoSoporte, + style: CustomLabels.h3, + textAlign: TextAlign.center, + ), + ), + Center( + child: Column( + children: [ + const Text( + 'Horario de atención:', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 10), + Text(settings.diasSoporte, style: CustomLabels.h3), + Text(settings.horasSoporte, style: CustomLabels.h3), + ], + ), + ), + const SizedBox(height: 30), + Padding( + padding: EdgeInsets.only( + left: size.width > 500 ? 30 : 0, + right: size.width > 500 ? 30 : 0, + bottom: 30, + ), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 15, + runSpacing: 15, + children: [ + _buildGridButton( + context, + icon: Icons.phone, + text: 'WhatsApp', + onPressed: () => + _goWhatssApp(settings.numeroSoporte), + ), + _buildGridButton( + context, + icon: Icons.email, + text: 'Email', + onPressed: () => _goEmail(settings.emailSoporte), + ), + _buildGridButton( + context, + icon: Icons.feedback, + text: 'Sugerencias', + onPressed: _goSugerencias, + ), + _buildGridButton( + context, + icon: Icons.privacy_tip, + text: 'Políticas de Privacidad', + onPressed: () => + _goPoliticas(settings.politicasPrivacidad), + ), + _buildGridButton( + context, + icon: Icons.article, + text: 'Términos y Condiciones', + onPressed: () => + _goTerminos(settings.terminosCondiciones), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildGridButton( + BuildContext context, { + required IconData icon, + required String text, + required VoidCallback onPressed, + }) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onPressed, + child: Container( + height: 110, + width: 180, + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 40, color: Colors.white), + const SizedBox(height: 10), + Center( + child: Text( + textAlign: TextAlign.center, + text, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart new file mode 100644 index 0000000..18a9b0f --- /dev/null +++ b/lib/utils/constants.dart @@ -0,0 +1 @@ +const String API_BASE_URL = String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:3000/api/v1'); diff --git a/lib/utils/local_notifications.dart b/lib/utils/local_notifications.dart new file mode 100644 index 0000000..e8d3e97 --- /dev/null +++ b/lib/utils/local_notifications.dart @@ -0,0 +1,27 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:prosapp_web_app/utils/constants.dart' show API_BASE_URL; + +class LocalNotifications { + static Future sendPushNotification( + String token, String title, String body) async { + try { + http.Response response = await http.post( + Uri.parse('$API_BASE_URL/notifications/send'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode( + { + 'title': title, + 'body': body, + 'to': token, + }, + ), + ); + response; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/src/components/network_utility.dart b/lib/utils/network_utility.dart similarity index 100% rename from lib/src/components/network_utility.dart rename to lib/utils/network_utility.dart diff --git a/lib/src/utils/time_of_day_extension.dart b/lib/utils/time_of_day_extension.dart similarity index 100% rename from lib/src/utils/time_of_day_extension.dart rename to lib/utils/time_of_day_extension.dart diff --git a/lib/src/utils/time_of_day_utils.dart b/lib/utils/time_of_day_utils.dart similarity index 85% rename from lib/src/utils/time_of_day_utils.dart rename to lib/utils/time_of_day_utils.dart index d4ce6e9..43c7818 100644 --- a/lib/src/utils/time_of_day_utils.dart +++ b/lib/utils/time_of_day_utils.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:prosappco/src/utils/time_of_day_extension.dart'; +import 'package:prosapp_web_app/utils/time_of_day_extension.dart'; class TimeOfDayUtils { static List genRanges(TimeOfDay timeStart, TimeOfDay timeEnd) { diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 5c420c5..ef6728f 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,10 +4,10 @@ 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 "prosappco") +set(BINARY_NAME "prosapp_web_app") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.prosappco") +set(APPLICATION_ID "com.example.prosapp_web_app") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. @@ -86,6 +86,7 @@ set_target_properties(${BINARY_NAME} 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) @@ -122,6 +123,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 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") diff --git a/linux/my_application.cc b/linux/my_application.cc index 4613912..9f93ac7 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { 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, "prosappco"); + gtk_header_bar_set_title(header_bar, "prosapp_web_app"); 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, "prosappco"); + gtk_window_set_title(window, "prosapp_web_app"); } gtk_window_set_default_size(window, 1280, 720); @@ -81,6 +81,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch 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); @@ -91,6 +109,8 @@ static void my_application_dispose(GObject* 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; } diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index 4b81f9b..c2efd0b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index 5caa9d1..c2efd0b 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6601d27..a172cdb 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,20 +5,22 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos -import flutter_local_notifications -import geolocator_apple -import location -import package_info_plus +import firebase_auth +import firebase_core +import firebase_storage +import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) - LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin")) - FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index c795730..0000000 --- a/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -platform :osx, '10.14' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 89569c5..d393af2 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,15 +21,22 @@ /* 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 */; }; - DFAE116AA0A5B93B39BF14BF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AE1007FC9B2143762CB9DF6D /* GoogleService-Info.plist */; }; /* 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 */; @@ -53,9 +60,11 @@ /* 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 /* prosappco.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = prosappco.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* prosapp_web_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "prosapp_web_app.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 = ""; }; @@ -69,10 +78,16 @@ 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 = ""; }; - AE1007FC9B2143762CB9DF6D /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -83,6 +98,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -99,16 +122,17 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - AE1007FC9B2143762CB9DF6D /* GoogleService-Info.plist */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* prosappco.app */, + 33CC10ED2044A3C60003C045 /* prosapp_web_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -158,6 +182,24 @@ /* 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" */; @@ -175,7 +217,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* prosappco.app */; + productReference = 33CC10ED2044A3C60003C045 /* prosapp_web_app.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -184,10 +226,15 @@ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; @@ -218,19 +265,26 @@ 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 */, - DFAE116AA0A5B93B39BF14BF /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -278,6 +332,14 @@ /* 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; @@ -291,6 +353,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; @@ -311,11 +378,54 @@ /* 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.prosappWebApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/prosapp_web_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/prosapp_web_app"; + }; + 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.prosappWebApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/prosapp_web_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/prosapp_web_app"; + }; + 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.prosappWebApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/prosapp_web_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/prosapp_web_app"; + }; + 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"; @@ -339,9 +449,11 @@ 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; @@ -389,6 +501,7 @@ 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"; @@ -412,9 +525,11 @@ 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; @@ -442,6 +557,7 @@ 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"; @@ -465,9 +581,11 @@ 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; @@ -541,6 +659,16 @@ /* 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 = ( diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 667e4a7..7ae7ce1 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -31,12 +31,23 @@ + + + + @@ -71,7 +82,7 @@ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 8052de7..2e6e9f1 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = prosappco +PRODUCT_NAME = prosapp_web_app // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.prosappco +PRODUCT_BUNDLE_IDENTIFIER = com.example.prosappWebApp // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/macos/Runner/GoogleService-Info.plist b/macos/Runner/GoogleService-Info.plist deleted file mode 100644 index 3b62a42..0000000 --- a/macos/Runner/GoogleService-Info.plist +++ /dev/null @@ -1,36 +0,0 @@ - - - - - CLIENT_ID - 245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn - ANDROID_CLIENT_ID - 245204384533-7p2j0go97da4gp062s7enop0ab8q7a7p.apps.googleusercontent.com - API_KEY - AIzaSyDUhnkNkeEPkqcwVo5EWr63q3oEX6bDGic - GCM_SENDER_ID - 245204384533 - PLIST_VERSION - 1 - BUNDLE_ID - com.example.prosappco - PROJECT_ID - prosapp-5747a - STORAGE_BUCKET - prosapp-5747a.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:245204384533:ios:aeb233e55c5ddf7a9f871c - - \ No newline at end of file diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 2722837..3cc05eb 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -3,7 +3,7 @@ import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() + let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift index 5418c9f..61f3bd1 100644 --- a/macos/RunnerTests/RunnerTests.swift +++ b/macos/RunnerTests/RunnerTests.swift @@ -1,5 +1,5 @@ -import FlutterMacOS import Cocoa +import FlutterMacOS import XCTest class RunnerTests: XCTestCase { diff --git a/macos/firebase_app_id_file.json b/macos/firebase_app_id_file.json deleted file mode 100644 index 1c89e48..0000000 --- a/macos/firebase_app_id_file.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "file_generated_by": "FlutterFire CLI", - "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", - "GOOGLE_APP_ID": "1:245204384533:ios:1d58f5624d0df4859f871c", - "FIREBASE_PROJECT_ID": "prosapp-5747a", - "GCM_SENDER_ID": "245204384533" -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f0cae0d..6f1f536 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,925 @@ { - "name": "prosappco", + "name": "ProsApp", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "dependencies": { + "firebase": "^10.12.2" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.4.tgz", + "integrity": "sha512-OJEl/8Oye/k+vJ1zV/1L6eGpc1XzAj+WG2TPznJ7PszL7sOFLBXkL9IjHfOCGDGpXeO3btozy/cYUqv4zgNeHg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/installations": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.10.tgz", + "integrity": "sha512-ia68RcLQLLMFWrM10JfmFod7eJGwqr4/uyrtzHpTDnxGX/6gNCBTOuxdAbyWIqXI5XmcMQdz9hDijGKOHgDfPw==", + "dependencies": { + "@firebase/analytics": "0.10.4", + "@firebase/analytics-types": "0.8.2", + "@firebase/component": "0.6.7", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.2.tgz", + "integrity": "sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==" + }, + "node_modules/@firebase/app": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.5.tgz", + "integrity": "sha512-iY/fNot+hWPk9sTX8aHMqlcX9ynRvpGkskWAdUZ2eQQdLo8d1hSFYcYNwPv0Q/frGMasw8udKWMcFOEpC9fG8g==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.4.tgz", + "integrity": "sha512-2tjRDaxcM5G7BEpytiDcIl+NovV99q8yEqRMKDbn4J4i/XjjuThuB4S+4PkmTnZiCbdLXQiBhkVxNlUDcfog5Q==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.11.tgz", + "integrity": "sha512-t01zaH3RJpKEey0nGduz3Is+uSz7Sj4U5nwOV6lWb+86s5xtxpIvBJzu/lKxJfYyfZ29eJwpdjEgT1/lm4iQyA==", + "dependencies": { + "@firebase/app-check": "0.8.4", + "@firebase/app-check-types": "0.5.2", + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.2.tgz", + "integrity": "sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.35.tgz", + "integrity": "sha512-vgay/WRjeH0r97/Q6L6df2CMx7oyNFDsE5yPQ9oR1G+zx2eT0s8vNNh0WlKqQxUEWaOLRnXhQ8gy7uu0cBgTRg==", + "dependencies": { + "@firebase/app": "0.10.5", + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.4.tgz", + "integrity": "sha512-d2Fw17s5QesojwebrA903el20Li9/YGgkoOGJjagM4I1qAT36APa/FcZ+OX86KxbYKCtQKTMqraU8pxG7C2JWA==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.9.tgz", + "integrity": "sha512-RX8Zh/3zz2CsVbmYfgHkfUm4fAEPCl+KHVIImNygV5jTGDF6oKOhBIpf4Yigclyu8ESQKZ4elyN0MBYm9/7zGw==", + "dependencies": { + "@firebase/auth": "1.7.4", + "@firebase/auth-types": "0.12.2", + "@firebase/component": "0.6.7", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.2.tgz", + "integrity": "sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.7.tgz", + "integrity": "sha512-baH1AA5zxfaz4O8w0vDwETByrKTQqB5CDjRls79Sa4eAGAoERw4Tnung7XbMl3jbJ4B/dmmtsMrdki0KikwDYA==", + "dependencies": { + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.5.tgz", + "integrity": "sha512-cAfwBqMQuW6HbhwI3Cb/gDqZg7aR0OmaJ85WUxlnoYW2Tm4eR0hFl5FEijI3/gYPUiUcUPQvTkGV222VkT7KPw==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.5.tgz", + "integrity": "sha512-NDSMaDjQ+TZEMDMmzJwlTL05kh1+0Y84C+kVMaOmNOzRGRM7VHi29I6YUhCetXH+/b1Wh4ZZRyp1CuWkd8s6hg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/database": "1.0.5", + "@firebase/database-types": "1.0.3", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.3.tgz", + "integrity": "sha512-39V/Riv2R3O/aUjYKh0xypj7NTNXNAK1bcgY5Kx+hdQPRS/aPTS8/5c0CGFYKgVuFbYlnlnhrCTYsh2uNhGwzA==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.6" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.6.3.tgz", + "integrity": "sha512-d/+N2iUsiJ/Dc7fApdpdmmTXzwuTCromsdA1lKwYfZtMIOd1fI881NSLwK2wV4I38wkLnvfKJUV6WpU1f3/ONg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "@firebase/webchannel-wrapper": "1.0.0", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.32", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.32.tgz", + "integrity": "sha512-at71mwK7a/mUXH0OgyY0+gUzedm/EUydDFYSFsBoO8DYowZ23Mgd6P4Rzq/Ll3zI/3xJN7LGe7Qp4iE/V/3Arg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/firestore": "4.6.3", + "@firebase/firestore-types": "3.0.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.2.tgz", + "integrity": "sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.5.tgz", + "integrity": "sha512-qrHJ+l62mZiU5UZiVi84t/iLXZlhRuSvBQsa2qvNLgPsEWR7wdpWhRmVdB7AU8ndkSHJjGlMICqrVnz47sgU7Q==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.7", + "@firebase/messaging-interop-types": "0.2.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.11.tgz", + "integrity": "sha512-Qn+ts/M6Lj2/6i1cp5V5TRR+Hi9kyXyHbo+w9GguINJ87zxrCe6ulx3TI5AGQkoQa8YFHUhT3DMGmLFiJjWTSQ==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/functions": "0.11.5", + "@firebase/functions-types": "0.6.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.2.tgz", + "integrity": "sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==" + }, + "node_modules/@firebase/installations": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.7.tgz", + "integrity": "sha512-i6iGoXRu5mX4rTsiMSSKrgh9pSEzD4hwBEzRh5kEhOTr8xN/wvQcCPZDSMVYKwM2XyCPBLVq0JzjyerwL0Rihg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/util": "1.9.6", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.7.tgz", + "integrity": "sha512-RPcbD+3nqHbnhVjIOpWK2H5qzZ8pAAAScceiWph0VNTqpKyPQ5tDcp4V5fS0ELpfgsHYvroMLDKfeHxpfvm8cw==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/installations": "0.6.7", + "@firebase/installations-types": "0.5.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.2.tgz", + "integrity": "sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.9.tgz", + "integrity": "sha512-IH+JJmzbFGZXV3+TDyKdqqKPVfKRqBBg2BfYYOy7cm7J+SwV+uJMe8EnDKYeQLEQhtpwciPfJ3qQXJs2lbxDTw==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/installations": "0.6.7", + "@firebase/messaging-interop-types": "0.2.2", + "@firebase/util": "1.9.6", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.9.tgz", + "integrity": "sha512-5jN6wyhwPgBH02zOtmmoOeyfsmoD7ty48D1m0vVPsFg55RqN2Z3Q9gkZ5GmPklFPjTPLcxB1ObcHOZvThTkm7g==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/messaging": "0.12.9", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz", + "integrity": "sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.7.tgz", + "integrity": "sha512-d+Q4ltjdJZqjzcdms5i0UC9KLYX7vKGcygZ+7zHA/Xk+bAbMD2CPU0nWTnlNFWifZWIcXZ/2mAMvaGMW3lypUA==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/installations": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.7.tgz", + "integrity": "sha512-cb8ge/5iTstxfIGW+iiY+7l3FtN8gobNh9JSQNZgLC9xmcfBYWEs8IeEWMI6S8T+At0oHc3lv+b2kpRMUWr8zQ==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/performance": "0.6.7", + "@firebase/performance-types": "0.2.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.2.tgz", + "integrity": "sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.7.tgz", + "integrity": "sha512-5oPNrPFLsbsjpq0lUEIXoDF2eJK7vAbyXe/DEuZQxnwJlfR7aQbtUlEkRgQWcicXpyDmAmDLo7q7lDbCYa6CpA==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/installations": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.7.tgz", + "integrity": "sha512-Fq0oneQ4SluLnfr5/HfzRS1TZf1ANj1rWbCCW3+oC98An3nE+sCdp+FSuHsEVNwgMg4Tkwx9Oom2lkKeU+Vn+w==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/remote-config": "0.4.7", + "@firebase/remote-config-types": "0.3.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz", + "integrity": "sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==" + }, + "node_modules/@firebase/storage": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.5.tgz", + "integrity": "sha512-nGWBOGFNr10j0LA4NJ3/Yh3us/lb0Q1xSIKZ38N6FcS+vY54nqJ7k3zE3PENregHC8+8txRow++A568G3v8hOA==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.8.tgz", + "integrity": "sha512-qDfY9kMb6Ch2hZb40sBjDQ8YPxbjGOxuT+gU1Z0iIVSSpSX0f4YpGJCypUXiA0T11n6InCXB+T/Dknh2yxVTkg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/storage": "0.12.5", + "@firebase/storage-types": "0.8.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.2.tgz", + "integrity": "sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.6.tgz", + "integrity": "sha512-IBr1MZbp4d5MjBCXL3TW1dK/PDXX4yOGbiwRNh1oAbE/+ci5Uuvy9KIrsFYY80as1I0iOaD5oOMA9Q8j4TJWcw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/vertexai-preview": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@firebase/vertexai-preview/-/vertexai-preview-0.0.2.tgz", + "integrity": "sha512-NOOL63kFQRq45ioi5P+hlqj/4LNmvn1URhGjQdvyV54c1Irvoq26aW861PRRLjrSMIeNeiLtCLD5pe+ediepAg==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.0.tgz", + "integrity": "sha512-zuWxyfXNbsKbm96HhXzainONPFqRcoZblQ++e9cAIGUuHfl2cFSBzW01jtesqWG/lqaUyX3H8O1y9oWboGNQBA==" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@types/node": { + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase": { + "version": "10.12.2", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.12.2.tgz", + "integrity": "sha512-ZxEdtSvP1I9su1yf32D8TIdgxtPgxwr6z3jYAR1TXS/t+fVfpoPc/N1/N2bxOco9mNjUoc+od34v5Fn4GeKs6Q==", + "dependencies": { + "@firebase/analytics": "0.10.4", + "@firebase/analytics-compat": "0.2.10", + "@firebase/app": "0.10.5", + "@firebase/app-check": "0.8.4", + "@firebase/app-check-compat": "0.3.11", + "@firebase/app-compat": "0.2.35", + "@firebase/app-types": "0.9.2", + "@firebase/auth": "1.7.4", + "@firebase/auth-compat": "0.5.9", + "@firebase/database": "1.0.5", + "@firebase/database-compat": "1.0.5", + "@firebase/firestore": "4.6.3", + "@firebase/firestore-compat": "0.3.32", + "@firebase/functions": "0.11.5", + "@firebase/functions-compat": "0.3.11", + "@firebase/installations": "0.6.7", + "@firebase/installations-compat": "0.2.7", + "@firebase/messaging": "0.12.9", + "@firebase/messaging-compat": "0.2.9", + "@firebase/performance": "0.6.7", + "@firebase/performance-compat": "0.2.7", + "@firebase/remote-config": "0.4.7", + "@firebase/remote-config-compat": "0.2.7", + "@firebase/storage": "0.12.5", + "@firebase/storage-compat": "0.3.8", + "@firebase/util": "1.9.6", + "@firebase/vertexai-preview": "0.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..d28e945 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "firebase": "^10.12.2" + } +} diff --git a/pubspec.lock b/pubspec.lock index 9d8a8bc..1060826 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,30 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - animate_do: - dependency: "direct main" - description: - name: animate_do - sha256: "9aeacc1a7238f971c039bdf45d13c628be554a242e0251c4ddda09d19a1a923f" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - animated_splash_screen: - dependency: "direct main" - description: - name: animated_splash_screen - sha256: f45634db6ec4e8cf034c53e03f3bd83898a16fe3c9286bf5510b6831dfcf2124 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - args: + _flutterfire_internals: dependency: transitive description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + name: _flutterfire_internals + sha256: a315d1c444402c3fa468de626d33a1c666041c87e9e195e8fb355b7084aefcc1 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "1.3.38" async: dependency: transitive description: @@ -49,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" clock: dependency: transitive description: @@ -65,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "1232370ad04c21c699d0e73b2dc2e1c3b49258f89a16f0119036fd3c6e8aa2f5" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: cfc64ae4a48bbb0ff6730b04f2d4653043c7a9b9008a991b1f2012a534b79e26 + url: "https://pub.dev" + source: hosted + version: "6.2.8" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "3db4e4c10feae18d80da86982781578c8e76cb6a43cf83110d8d6a62af9a952a" + url: "https://pub.dev" + source: hosted + version: "4.0.2" collection: dependency: transitive description: @@ -73,22 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - community_material_icon: - dependency: "direct main" - description: - name: community_material_icon - sha256: bb389689f6278158d7b9d9b0c9433e603933283104fea226594590f61503fd08 - url: "https://pub.dev" - source: hosted - version: "5.9.55" cross_file: dependency: transitive description: name: cross_file - sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+6" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -97,38 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - csslib: - dependency: transitive - description: - name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" - url: "https://pub.dev" - source: hosted - version: "1.0.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" - dbus: - dependency: transitive - description: - name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" - url: "https://pub.dev" - source: hosted - version: "0.7.8" - diacritic: + version: "1.0.8" + email_validator: dependency: "direct main" description: - name: diacritic - sha256: a84e03ec2779375fb86430dbe9d8fba62c68376f2499097a5f6e75556babe706 + name: email_validator + sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "3.0.0" fake_async: dependency: transitive description: @@ -141,26 +117,26 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: name: file_picker - sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "8.0.6" file_selector_linux: dependency: transitive description: @@ -173,18 +149,18 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" file_selector_windows: dependency: transitive description: @@ -193,88 +169,115 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "087fdcb54b0af6f4c5c756e1db4f90e9b65871b9b3a75fabaa0e0ee578301669" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "8fac689f71ac3489a785579e99a4bad24a93ad3d78c313fb786ee517012d25f1" + url: "https://pub.dev" + source: hosted + version: "7.4.1" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "486b2527fcfcab01278378d8d4791f4f7bee8a9f15bf35e801ba08fbdd84c234" + url: "https://pub.dev" + source: hosted + version: "5.12.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "1e06b0538ab3108a61d895ee16951670b491c4a94fce8f2d30e5de7a5eca4b28" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d" + url: "https://pub.dev" + source: hosted + version: "2.17.2" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: "01fdcef335d2c86a265e9eb3c5b3b489ddc5589e7fc18a74df84b31be9f0c15b" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: "8fdbfc63af3441434f33a420a304a670b464ff71b8f12fccc80accba1378ca3b" + url: "https://pub.dev" + source: hosted + version: "5.1.25" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: bb9a387ae26bf7358e118f191ee22167b30217cad551184592cc73e7f73bd470 + url: "https://pub.dev" + source: hosted + version: "3.9.10" + fluro: + dependency: "direct main" + description: + name: fluro + sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" + url: "https://pub.dev" + source: hosted + version: "2.0.5" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_animate: + flutter_dotenv: dependency: "direct main" description: - name: flutter_animate - sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" url: "https://pub.dev" source: hosted - version: "4.2.0+1" - flutter_dialogs: - dependency: "direct main" - description: - name: flutter_dialogs - sha256: d654a7ca2080223a368b187b748ee9f06c0fe16a4e2d3bde455a0954b5ddb48d - url: "https://pub.dev" - source: hosted - version: "3.0.0" + version: "5.1.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "2.0.3" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "53c332ecee8e4d723269c1c2d0cdf7cbbff0a66cc0554d230a6f38cae81760d1" - url: "https://pub.dev" - source: hosted - version: "14.1.4" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" - url: "https://pub.dev" - source: hosted - version: "4.0.0+1" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" - url: "https://pub.dev" - source: hosted - version: "7.0.0+1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_otp_text_field: - dependency: "direct main" - description: - name: flutter_otp_text_field - sha256: c9164ba391071fb9783698256ddbb9efc960cfed056d843db164711c65b1b114 - url: "https://pub.dev" - source: hosted - version: "1.1.1" + version: "3.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e url: "https://pub.dev" source: hosted - version: "2.0.17" - flutter_polyline_points: - dependency: "direct main" - description: - name: flutter_polyline_points - sha256: "0e07862139cb65a88789cd6efbe284c0b6f1fcb5ed5ec87781759c48190d84b9" - url: "https://pub.dev" - source: hosted - version: "2.0.0" + version: "2.0.20" flutter_rating_bar: dependency: "direct main" description: @@ -293,150 +296,30 @@ packages: description: flutter source: sdk version: "0.0.0" - geocoding: + get_it: dependency: "direct main" description: - name: geocoding - sha256: e1dc0ac56666d9ed1d5a9ae5543ce9eb5986db6209cc7600103487d09192059c + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 url: "https://pub.dev" source: hosted - version: "2.1.1" - geocoding_android: - dependency: transitive - description: - name: geocoding_android - sha256: "609db1d71bc364dd9d0616f72a41c01e0c74f3a3807efb85e0d5a67e57baf50f" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - geocoding_ios: - dependency: transitive - description: - name: geocoding_ios - sha256: c85495ce8fb34e4fbd2dd8fc5f79263d622d9f88c4af948c965daf6b27a7f3a1 - url: "https://pub.dev" - source: hosted - version: "2.1.0" - geocoding_platform_interface: - dependency: transitive - description: - name: geocoding_platform_interface - sha256: "8848605d307d844d89937cdb4b8ad7dfa880552078f310fa24d8a460f6dddab4" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - geolocator: + version: "7.7.0" + google_fonts: dependency: "direct main" description: - name: geolocator - sha256: "5c23f3613f50586c0bbb2b8f970240ae66b3bd992088cf60dd5ee2e6f7dde3a8" + name: google_fonts + sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d" url: "https://pub.dev" source: hosted - version: "9.0.2" - geolocator_android: - dependency: transitive - description: - name: geolocator_android - sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" - url: "https://pub.dev" - source: hosted - version: "4.3.1" - geolocator_apple: - dependency: transitive - description: - name: geolocator_apple - sha256: ab90ae811c42ec2f6021e01eca71df00dee6ff1e69d2c2dafd4daeb0b793f73d - url: "https://pub.dev" - source: hosted - version: "2.3.2" - geolocator_platform_interface: - dependency: transitive - description: - name: geolocator_platform_interface - sha256: b7aca62aa05d7e610c396a53a1936ff87fce2f735d76e93fde9269c341c46a25 - url: "https://pub.dev" - source: hosted - version: "4.1.1" - geolocator_web: - dependency: transitive - description: - name: geolocator_web - sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - geolocator_windows: - dependency: transitive - description: - name: geolocator_windows - sha256: "4f4218f122a6978d0ad655fa3541eea74c67417440b09f0657238810d5af6bdc" - url: "https://pub.dev" - source: hosted - version: "0.1.3" - google_maps: - dependency: transitive - description: - name: google_maps - sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" - url: "https://pub.dev" - source: hosted - version: "8.2.0" - google_maps_flutter: - dependency: "direct main" - description: - name: google_maps_flutter - sha256: f4040715c2998144a46cfc0fd91ee83226655af069dfed3d2b84552dc5e4facb - url: "https://pub.dev" - source: hosted - version: "2.17.1" - google_maps_flutter_android: - dependency: transitive - description: - name: google_maps_flutter_android - sha256: a404c67d460f64b8bdc48aa5e39c81aa13505552d8783ae68f9aeef074913286 - url: "https://pub.dev" - source: hosted - version: "2.19.12" - google_maps_flutter_ios: - dependency: transitive - description: - name: google_maps_flutter_ios - sha256: f573749a75c4074b7a74ab986703607e9cd859fb2807c332872620f3656400dc - url: "https://pub.dev" - source: hosted - version: "2.18.3" - google_maps_flutter_platform_interface: - dependency: transitive - description: - name: google_maps_flutter_platform_interface - sha256: ddbe34435dfb34e83fca295c6a8dcc53c3b51487e9eec3c737ce4ae605574347 - url: "https://pub.dev" - source: hosted - version: "2.15.0" - google_maps_flutter_web: - dependency: transitive - description: - name: google_maps_flutter_web - sha256: e23f2869449bb3e2ed7cfdb178d716e1da7edc684363987e4cb2c42f2bd7a9e0 - url: "https://pub.dev" - source: hosted - version: "0.6.2+3" - html: - dependency: transitive - description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" + version: "8.1.0" http: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_parser: dependency: transitive description: @@ -449,90 +332,74 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.1.2" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + sha256: "4161e1f843d8480d2e9025ee22411778c3c9eb7e40076dcf2da23d8242b7b51c" url: "https://pub.dev" source: hosted - version: "0.8.13+19" + version: "0.8.12+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.0.4" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.12" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.1+1" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "0.2.1+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.11.1" + version: "2.10.0" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.1+1" intl: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" - intl_phone_field: - dependency: "direct main" - description: - name: intl_phone_field - sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" + version: "0.19.0" leak_tracker: dependency: transitive description: @@ -561,34 +428,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" - location: - dependency: "direct main" - description: - name: location - sha256: "06be54f682c9073cbfec3899eb9bc8ed90faa0e17735c9d9fa7fe426f5be1dd1" - url: "https://pub.dev" - source: hosted - version: "5.0.3" - location_platform_interface: - dependency: transitive - description: - name: location_platform_interface - sha256: "8aa1d34eeecc979d7c9fe372931d84f6d2ebbd52226a54fe1620de6fdc0753b1" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - location_web: - dependency: transitive - description: - name: location_web - sha256: ec484c66e8a4ff1ee5d044c203f4b6b71e3a0556a97b739a5bc9616de672412b - url: "https://pub.dev" - source: hosted - version: "4.2.0" + version: "3.0.0" matcher: dependency: transitive description: @@ -617,10 +460,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" nested: dependency: transitive description: @@ -629,30 +472,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" - url: "https://pub.dev" - source: hosted - version: "4.2.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - page_transition: - dependency: transitive - description: - name: page_transition - sha256: dee976b1f23de9bbef5cd512fe567e9f6278caee11f5eaca9a2115c19dc49ef6 - url: "https://pub.dev" - source: hosted - version: "2.1.0" path: dependency: transitive description: @@ -661,6 +480,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a + url: "https://pub.dev" + source: hosted + version: "2.2.6" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -673,10 +516,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -685,22 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 - url: "https://pub.dev" - source: hosted - version: "5.4.0" platform: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -713,50 +548,34 @@ packages: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" - responsive_builder: - dependency: "direct main" - description: - name: responsive_builder - sha256: a38ba9ba86c9daf08904674553034b651377b1d685d10ee450d8350ae51f76ec - url: "https://pub.dev" - source: hosted - version: "0.7.0" - sanitize_html: - dependency: transitive - description: - name: sanitize_html - sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" - url: "https://pub.dev" - source: hosted - version: "2.1.0" + version: "6.1.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.4.0" shared_preferences_linux: dependency: transitive description: @@ -769,18 +588,18 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -789,22 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - shimmer: - dependency: "direct main" - description: - name: shimmer - sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" - url: "https://pub.dev" - source: hosted - version: "3.0.0" simple_gesture_detector: dependency: transitive description: name: simple_gesture_detector - sha256: "86d08f85f1f58583b7b4b941d989f48ea6ce08c1724a1d10954a277c2ec36592" + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" sky_engine: dependency: transitive description: flutter @@ -814,10 +625,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -834,14 +645,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" string_scanner: dependency: transitive description: @@ -854,10 +657,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" + sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.1.2" term_glyph: dependency: transitive description: @@ -874,14 +677,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" - timezone: - dependency: transitive - description: - name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" - url: "https://pub.dev" - source: hosted - version: "0.9.2" typed_data: dependency: transitive description: @@ -890,62 +685,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - universal_html: - dependency: "direct main" - description: - name: universal_html - sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" - url: "https://pub.dev" - source: hosted - version: "2.2.4" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf url: "https://pub.dev" source: hosted - version: "6.3.32" + version: "6.3.3" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: @@ -958,26 +737,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.3.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.1.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.dev" - source: hosted - version: "3.0.7" + version: "3.1.1" vector_math: dependency: transitive description: @@ -990,74 +761,34 @@ packages: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "14.2.1" web: dependency: transitive description: name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "1.1.1" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e - url: "https://pub.dev" - source: hosted - version: "4.4.1" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff - url: "https://pub.dev" - source: hosted - version: "3.12.0" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: b4b42295b3aa91ed22ba6d3dd7de56efbb8f3ab3d6e41d8b1615d13537c7801d - url: "https://pub.dev" - source: hosted - version: "3.9.2" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.5.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" - xml: - dependency: transitive - description: - name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" - url: "https://pub.dev" - source: hosted - version: "6.3.0" + version: "1.0.4" sdks: - dart: ">=3.12.0 <4.0.0" - flutter: ">=3.44.0" + dart: ">=3.10.0-0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 78c4f07..7960207 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ -name: prosappco -description: Encuentra personal médico certificado. +name: prosapp_web_app +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 +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 @@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # 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.11+11 +version: 1.0.0+1 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ">=3.4.1 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -28,93 +28,65 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + cloud_firestore: ^5.0.2 + cupertino_icons: ^1.0.6 + email_validator: ^3.0.0 + file_picker: ^8.0.6 + firebase_auth: ^5.1.1 + firebase_core: ^3.1.1 + firebase_storage: ^12.1.0 + fluro: ^2.0.5 flutter: sdk: flutter - intl: any - flutter_localizations: - sdk: flutter - shared_preferences: ^2.0.10 - cupertino_icons: ^1.0.2 - location: ^5.0.3 - flutter_polyline_points: ^2.0.0 - google_maps_flutter: ^2.9.0 - file_picker: ^5.3.2 - provider: ^6.0.5 - package_info_plus: ^4.2.0 - flutter_otp_text_field: - intl_phone_field: - diacritic: + flutter_dotenv: ^5.1.0 + get_it: ^7.7.0 + google_fonts: ^8.1.0 image_picker: ^1.1.2 - flutter_animate: - flutter_rating_bar: - table_calendar: - http: ^1.1.0 - geolocator: ^9.0.2 - geocoding: ^2.1.0 + intl: ^0.19.0 + provider: ^6.1.2 + shared_preferences: ^2.2.3 + table_calendar: ^3.1.2 url_launcher: ^6.3.0 - community_material_icon: ^5.9.55 - webview_flutter: ^4.4.1 - responsive_builder: ^0.7.0 - flutter_local_notifications: ^14.1.1 - flutter_dialogs: ^3.0.0 - universal_html: ^2.2.3 - animated_splash_screen: ^1.3.0 - animate_do: ^3.0.2 - shimmer: ^3.0.0 - + flutter_rating_bar: ^4.0.1 + http: ^1.2.1 dev_dependencies: + flutter_lints: ^3.0.0 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: ^2.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: - - images/ - # - images/a_dot_ham.jpeg - + - assets/ + - .env # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware - # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages - # 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: Poppins - fonts: - - asset: fonts/Poppins-Thin.ttf - - asset: fonts/Poppins-ExtraLight.ttf - - asset: fonts/Poppins-Light.ttf - - asset: fonts/Poppins-Regular.ttf - - asset: fonts/Poppins-Medium.ttf - - asset: fonts/Poppins-Black.ttf - - asset: fonts/Poppins-SemiBold.ttf - - asset: fonts/Poppins-Bold.ttf - - asset: fonts/Poppins-ExtraBold.ttf - # style: italic - + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index f6e88ca..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// 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:prosappco/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/web/favicon.png b/web/favicon.png index 183fac2..9ce5a18 100644 Binary files a/web/favicon.png and b/web/favicon.png differ diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js deleted file mode 100644 index 5aaa7c4..0000000 --- a/web/firebase-messaging-sw.js +++ /dev/null @@ -1,19 +0,0 @@ -importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"); -importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js"); - -firebase.initializeApp({ -apiKey: "AIzaSyCNpUV_4cMEL9QZx7NESXK7QAlRjRGwx_Y", - authDomain: "prosapp-5747a.firebaseapp.com", - databaseURL: "https://prosapp-5747a-default-rtdb.firebaseio.com", - projectId: "prosapp-5747a", - storageBucket: "prosapp-5747a.appspot.com", - messagingSenderId: "245204384533", - appId: "1:245204384533:web:1b8f0b4c1d9ae21d9f871c", -}); - -const messaging = firebase.messaging(); - -// Optional: -messaging.onBackgroundMessage((message) => { - console.log("onBackgroundMessage", message); -}); \ No newline at end of file diff --git a/web/index.html b/web/index.html index 8218344..eb91e4e 100644 --- a/web/index.html +++ b/web/index.html @@ -1,70 +1,36 @@ - - + - - - - + - - Prosapp + prosapp_web_app - - - - - - - + + + + + + + + +
+ + diff --git a/web/manifest.json b/web/manifest.json index 3cc11cc..bbbdaf5 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "prosappco", - "short_name": "prosappco", + "name": "prosapp_web_app", + "short_name": "prosapp_web_app", "start_url": ".", "display": "standalone", "background_color": "#0175C2", diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index bf6553d..dd71e8b 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,14 +1,14 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(prosappco LANGUAGES CXX) +project(prosapp_web_app 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 "prosappco") +set(BINARY_NAME "prosapp_web_app") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. -cmake_policy(SET CMP0063 NEW) +cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) @@ -52,6 +52,7 @@ 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) @@ -86,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) 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") diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d207..903f489 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # 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") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6514907..3e6795e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,24 @@ #include "generated_plugin_registrant.h" +#include #include -#include +#include +#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); - GeolocatorWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("GeolocatorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseStoragePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c193e6e..9615bc9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore file_selector_windows - geolocator_windows + firebase_auth + firebase_core + firebase_storage url_launcher_windows ) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index caac1df..52fbde5 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "prosappco" "\0" + VALUE "FileDescription", "prosapp_web_app" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "prosappco" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "prosappco.exe" "\0" - VALUE "ProductName", "prosappco" "\0" + VALUE "InternalName", "prosapp_web_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "prosapp_web_app.exe" "\0" + VALUE "ProductName", "prosapp_web_app" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b25e363..955ee30 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { 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; } diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 707f7ba..1f3d2ee 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"prosappco", origin, size)) { + if (!window.Create(L"prosapp_web_app", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp index f5bf9fa..3a0b465 100644 --- a/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -45,9 +45,11 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } - int target_length = ::WideCharToMultiByte( + unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); + -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; @@ -55,8 +57,7 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index 041a385..60608d0 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -60,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h index c86632d..e901dde 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -77,7 +77,7 @@ class Win32Window { // 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 - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message,