diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 226d4dd..2987d3b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.ejada.hmg" // minSdk = 24 minSdk = 26 - targetSdk = 35 - compileSdk = 35 + targetSdk = 36 + compileSdk = 36 // targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName @@ -156,16 +156,16 @@ dependencies { implementation("com.intuit.ssp:ssp-android:1.1.0") implementation("com.intuit.sdp:sdp-android:1.1.0") -// implementation("com.github.bumptech.glide:glide:4.16.0") -// annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") + implementation("com.github.bumptech.glide:glide:4.16.0") + annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") implementation("com.mapbox.maps:android:11.5.0") // implementation("com.mapbox.maps:android:11.4.0") // AARs -// implementation(files("libs/PenNavUI.aar")) -// implementation(files("libs/Penguin.aar")) -// implementation(files("libs/PenguinRenderer.aar")) + implementation(files("libs/PenNavUI.aar")) + implementation(files("libs/Penguin.aar")) + implementation(files("libs/PenguinRenderer.aar")) implementation("com.github.kittinunf.fuel:fuel:2.3.1") implementation("com.github.kittinunf.fuel:fuel-android:2.3.1") @@ -180,9 +180,11 @@ dependencies { implementation("com.google.android.material:material:1.12.0") implementation("pl.droidsonroids.gif:android-gif-drawable:1.2.25") + implementation("com.mapbox.mapboxsdk:mapbox-sdk-turf:7.3.1") androidTestImplementation("androidx.test:core:1.6.1") implementation("com.whatsapp.otp:whatsapp-otp-android-sdk:0.1.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") // implementation(project(":vitalSignEngine")) + } \ No newline at end of file diff --git a/android/app/libs/PenNavUI.aar b/android/app/libs/PenNavUI.aar index d423bc1..7832df8 100644 Binary files a/android/app/libs/PenNavUI.aar and b/android/app/libs/PenNavUI.aar differ diff --git a/android/app/libs/Penguin.aar b/android/app/libs/Penguin.aar index 5c789c6..a769c7a 100644 Binary files a/android/app/libs/Penguin.aar and b/android/app/libs/Penguin.aar differ diff --git a/android/app/libs/PenguinRenderer.aar b/android/app/libs/PenguinRenderer.aar index b657ac6..2926e9a 100644 Binary files a/android/app/libs/PenguinRenderer.aar and b/android/app/libs/PenguinRenderer.aar differ diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4f7ef74..8c1388b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,7 +49,7 @@ - + @@ -58,6 +58,13 @@ + + + + + + + , + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } + val intent = Intent("PERMISSION_RESULT_ACTION").apply { + putExtra("PERMISSION_GRANTED", granted) + } + sendBroadcast(intent) + + // Log the request code and permission results + Log.d("PermissionsResult", "Request Code: $requestCode") + Log.d("PermissionsResult", "Permissions: ${permissions.joinToString()}") + Log.d("PermissionsResult", "Grant Results: ${grantResults.joinToString()}") + + } + + override fun onResume() { + super.onResume() + } +} diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PenguinInPlatformBridge.kt b/android/app/src/main/kotlin/com/ejada/hmg/PenguinInPlatformBridge.kt new file mode 100644 index 0000000..4df25bc --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PenguinInPlatformBridge.kt @@ -0,0 +1,61 @@ +package com.ejada.hmg.penguin + +import com.ejada.hmg.MainActivity +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.ejada.hmg.penguin.PenguinView +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import com.ejada.hmg.PermissionManager.HostNotificationPermissionManager +import com.ejada.hmg.PermissionManager.HostBgLocationManager +import com.ejada.hmg.PermissionManager.HostGpsStateManager +import io.flutter.plugin.common.MethodChannel + +class PenguinInPlatformBridge( + private var flutterEngine: FlutterEngine, + private var mainActivity: MainActivity +) { + + private lateinit var channel: MethodChannel + + companion object { + private const val CHANNEL = "launch_penguin_ui" + } + + @RequiresApi(Build.VERSION_CODES.O) + fun create() { +// openTok = OpenTok(mainActivity, flutterEngine) + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "launchPenguin" -> { + print("the platform channel is being called") + + if (HostNotificationPermissionManager.isNotificationPermissionGranted(mainActivity)) + else HostNotificationPermissionManager.requestNotificationPermission(mainActivity) + HostBgLocationManager.requestLocationBackgroundPermission(mainActivity) + HostGpsStateManager.requestLocationPermission(mainActivity) + val args = call.arguments as Map? + Log.d("TAG", "configureFlutterEngine: $args") + println("args") + args?.let { + PenguinView( + mainActivity, + 100, + args, + flutterEngine.dartExecutor.binaryMessenger, + activity = mainActivity, + channel + ) + } + } + + else -> { + result.notImplemented() + } + } + } + } + +} diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/AppPreferences.java b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/AppPreferences.java new file mode 100644 index 0000000..d012799 --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/AppPreferences.java @@ -0,0 +1,139 @@ +package com.ejada.hmg.PermissionManager; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.HandlerThread; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + + +/** + * This preferences for app level + */ + +public class AppPreferences { + + public static final String PREF_NAME = "PenguinINUI_AppPreferences"; + public static final int MODE = Context.MODE_PRIVATE; + + public static final String campusIdKey = "campusId"; + + public static final String LANG = "Lang"; + + public static final String settingINFO = "SETTING-INFO"; + + public static final String userName = "userName"; + public static final String passWord = "passWord"; + + private static HandlerThread handlerThread; + private static Handler handler; + + static { + handlerThread = new HandlerThread("PreferencesHandlerThread"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + } + + + + public static SharedPreferences getPreferences(final Context context) { + return context.getSharedPreferences(AppPreferences.PREF_NAME, AppPreferences.MODE); + } + + public static SharedPreferences.Editor getEditor(final Context context) { + return getPreferences(context).edit(); + } + + + public static void writeInt(final Context context, final String key, final int value) { + handler.post(() -> { + SharedPreferences.Editor editor = getEditor(context); + editor.putInt(key, value); + editor.apply(); + }); + } + + + public static int readInt(final Context context, final String key, final int defValue) { + Callable callable = () -> { + SharedPreferences preferences = getPreferences(context); + return preferences.getInt(key, -1); + }; + + Future future = new FutureTask<>(callable); + handler.post((Runnable) future); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Handle the exception appropriately + } + + return -1; // Return the default value in case of an error + } + + public static int getCampusId(final Context context) { + return readInt(context,campusIdKey,-1); + } + + + + public static void writeString(final Context context, final String key, final String value) { + handler.post(() -> { + SharedPreferences.Editor editor = getEditor(context); + editor.putString(key, value); + editor.apply(); + }); + } + + + public static String readString(final Context context, final String key, final String defValue) { + Callable callable = () -> { + SharedPreferences preferences = getPreferences(context); + return preferences.getString(key, defValue); + }; + + Future future = new FutureTask<>(callable); + handler.post((Runnable) future); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Handle the exception appropriately + } + + return defValue; // Return the default value in case of an error + } + + + public static void writeBoolean(final Context context, final String key, final boolean value) { + handler.post(() -> { + SharedPreferences.Editor editor = getEditor(context); + editor.putBoolean(key, value); + editor.apply(); + }); + } + + public static boolean readBoolean(final Context context, final String key, final boolean defValue) { + Callable callable = () -> { + SharedPreferences preferences = getPreferences(context); + return preferences.getBoolean(key, defValue); + }; + + Future future = new FutureTask<>(callable); + handler.post((Runnable) future); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Handle the exception appropriately + } + + return defValue; // Return the default value in case of an error + } + +} diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostBgLocationManager.java b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostBgLocationManager.java new file mode 100644 index 0000000..5bc332d --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostBgLocationManager.java @@ -0,0 +1,136 @@ +package com.ejada.hmg.PermissionManager; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.Settings; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.peng.pennavmap.PlugAndPlaySDK; +import com.peng.pennavmap.R; +import com.peng.pennavmap.enums.InitializationErrorType; + +/** + * Manages background location permission requests and handling for the application. + */ +public class HostBgLocationManager { + /** + * Request code for background location permission + */ + public static final int REQUEST_ACCESS_BACKGROUND_LOCATION_CODE = 301; + + /** + * Request code for navigating to app settings + */ + private static final int REQUEST_CODE_SETTINGS = 11234; + + /** + * Alert dialog for denied permissions + */ + private static AlertDialog deniedAlertDialog; + + /** + * Checks if the background location permission has been granted. + * + * @param context the context of the application or activity + * @return true if the permission is granted, false otherwise + */ + + public static boolean isLocationBackgroundGranted(Context context) { + return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Requests the background location permission from the user. + * + * @param activity the activity from which the request is made + */ + public static void requestLocationBackgroundPermission(Activity activity) { + // Check if the ACCESS_BACKGROUND_LOCATION permission is already granted + if (!isLocationBackgroundGranted(activity)) { + // Permission is not granted, so request it + ActivityCompat.requestPermissions(activity, + new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, + REQUEST_ACCESS_BACKGROUND_LOCATION_CODE); + } + } + + /** + * Displays a dialog prompting the user to grant the background location permission. + * + * @param activity the activity where the dialog is displayed + */ + public static void showLocationBackgroundPermission(Activity activity) { + AlertDialog alertDialog = new AlertDialog.Builder(activity) + .setCancelable(false) + .setMessage(activity.getString(R.string.com_penguin_nav_ui_geofence_alert_msg)) + .setPositiveButton(activity.getString(R.string.com_penguin_nav_ui_go_to_settings), (dialog, which) -> { + if (activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) { + HostBgLocationManager.requestLocationBackgroundPermission(activity); + } else { + openAppSettings(activity); + } + if (dialog != null) { + dialog.dismiss(); + } + }) + .setNegativeButton(activity.getString(R.string.com_penguin_nav_ui_later), (dialog, which) -> { + dialog.cancel(); + }) + .create(); + + alertDialog.show(); + } + + /** + * Handles the scenario where permissions are denied by the user. + * Displays a dialog to guide the user to app settings or exit the activity. + * + * @param activity the activity where the dialog is displayed + */ + public static synchronized void handlePermissionsDenied(Activity activity) { + if (deniedAlertDialog != null && deniedAlertDialog.isShowing()) { + deniedAlertDialog.dismiss(); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setCancelable(false) + .setMessage(activity.getString(R.string.com_penguin_nav_ui_permission_denied_dialog_msg)) + .setNegativeButton(activity.getString(R.string.com_penguin_nav_ui_cancel), (dialogInterface, i) -> { + if (PlugAndPlaySDK.externalPenNavUIDelegate != null) { + PlugAndPlaySDK.externalPenNavUIDelegate.onPenNavInitializationError( + InitializationErrorType.permissions.getTypeKey(), + InitializationErrorType.permissions); + } + activity.finish(); + }) + .setPositiveButton(activity.getString(R.string.com_penguin_nav_ui_go_settings), (dialogInterface, i) -> { + dialogInterface.dismiss(); + openAppSettings(activity); + }); + deniedAlertDialog = builder.create(); + deniedAlertDialog.show(); + } + + /** + * Opens the application's settings screen to allow the user to modify permissions. + * + * @param activity the activity from which the settings screen is launched + */ + private static void openAppSettings(Activity activity) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", activity.getPackageName(), null); + intent.setData(uri); + + if (intent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(intent, REQUEST_CODE_SETTINGS); + } + } +} diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostGpsStateManager.java b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostGpsStateManager.java new file mode 100644 index 0000000..adde120 --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostGpsStateManager.java @@ -0,0 +1,68 @@ +package com.ejada.hmg.PermissionManager; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.peng.pennavmap.managers.permissions.managers.BgLocationManager; + +public class HostGpsStateManager { + private static final int LOCATION_PERMISSION_REQUEST_CODE = 1; + + + public boolean checkGPSEnabled(Activity activity) { + LocationManager gpsStateManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE); + return gpsStateManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + } + + public static boolean isGpsGranted(Activity activity) { + return BgLocationManager.isLocationBackgroundGranted(activity) + || ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED; + } + + + /** + * Checks if the location permission is granted. + * + * @param activity the Activity context + * @return true if permission is granted, false otherwise + */ + public static boolean isLocationPermissionGranted(Activity activity) { + return ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED; + } + + /** + * Requests the location permission. + * + * @param activity the Activity context + */ + public static void requestLocationPermission(Activity activity) { + ActivityCompat.requestPermissions( + activity, + new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + }, + LOCATION_PERMISSION_REQUEST_CODE + ); + } +} diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostNotificationPermissionManager.java b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostNotificationPermissionManager.java new file mode 100644 index 0000000..5b9f19e --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/HostNotificationPermissionManager.java @@ -0,0 +1,73 @@ +package com.ejada.hmg.PermissionManager; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationManagerCompat; + +public class HostNotificationPermissionManager { + private static final int REQUEST_NOTIFICATION_PERMISSION = 100; + + + /** + * Checks if the notification permission is granted. + * + * @return true if the notification permission is granted, false otherwise. + */ + public static boolean isNotificationPermissionGranted(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + return ActivityCompat.checkSelfPermission(activity, android.Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED; + } catch (Exception e) { + // Handle cases where the API is unavailable + e.printStackTrace(); + return NotificationManagerCompat.from(activity).areNotificationsEnabled(); + } + } else { + // Permissions were not required below Android 13 for notifications + return NotificationManagerCompat.from(activity).areNotificationsEnabled(); + } + } + + /** + * Requests the notification permission. + */ + public static void requestNotificationPermission(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (!isNotificationPermissionGranted(activity)) { + ActivityCompat.requestPermissions(activity, + new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_NOTIFICATION_PERMISSION); + } + } + } + + /** + * Handles the result of the permission request. + * + * @param requestCode The request code passed in requestPermissions(). + * @param permissions The requested permissions. + * @param grantResults The grant results for the corresponding permissions. + */ + public static boolean handlePermissionResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (permissions.length > 0 && + permissions[0].equals(android.Manifest.permission.POST_NOTIFICATIONS) && + grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted + System.out.println("Notification permission granted."); + return true; + } else { + // Permission denied + System.out.println("Notification permission denied."); + return false; + } + + } + + +} diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionHelper.kt b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionHelper.kt new file mode 100644 index 0000000..9856a49 --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionHelper.kt @@ -0,0 +1,28 @@ +package com.ejada.hmg.PermissionManager + +import android.Manifest +import android.os.Build + +object PermissionHelper { + + fun getRequiredPermissions(): Array { + val permissions = mutableListOf( + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_NETWORK_STATE, + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, +// Manifest.permission.ACTIVITY_RECOGNITION + ) + + // For Android 12 (API level 31) and above, add specific permissions +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31) and above + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + permissions.add(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS) +// } + + return permissions.toTypedArray() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionManager.kt b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionManager.kt new file mode 100644 index 0000000..d8aea7b --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionManager.kt @@ -0,0 +1,50 @@ +package com.ejada.hmg.PermissionManager + +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +class PermissionManager( + private val context: Context, + val listener: PermissionListener, + private val requestCode: Int, + vararg permissions: String +) { + + private val permissionsArray = permissions + + interface PermissionListener { + fun onPermissionGranted() + fun onPermissionDenied() + } + + fun arePermissionsGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + permissionsArray.all { + ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + } else { + true + } + } + + fun requestPermissions(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ActivityCompat.requestPermissions(activity, permissionsArray, requestCode) + } + } + + fun handlePermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (this.requestCode == requestCode) { + val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } + if (allGranted) { + listener.onPermissionGranted() + } else { + listener.onPermissionDenied() + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionResultReceiver.kt b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionResultReceiver.kt new file mode 100644 index 0000000..c07d1de --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/PermissionManager/PermissionResultReceiver.kt @@ -0,0 +1,15 @@ +package com.ejada.hmg.PermissionManager + +// PermissionResultReceiver.kt +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class PermissionResultReceiver( + private val callback: (Boolean) -> Unit +) : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val granted = intent?.getBooleanExtra("PERMISSION_GRANTED", false) ?: false + callback(granted) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinMethod.kt b/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinMethod.kt new file mode 100644 index 0000000..18463d2 --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinMethod.kt @@ -0,0 +1,13 @@ +package com.ejada.hmg.penguin + +enum class PenguinMethod { + // initializePenguin("initializePenguin"), + // configurePenguin("configurePenguin"), + // showPenguinUI("showPenguinUI"), + // onPenNavUIDismiss("onPenNavUIDismiss"), + // onReportIssue("onReportIssue"), + // onPenNavSuccess("onPenNavSuccess"), + onPenNavInitializationError // onLocationOffCampus("onLocationOffCampus"), + // navigateToPOI("navigateToPOI"), + // openSharedLocation("openSharedLocation"); +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinNavigator.kt b/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinNavigator.kt new file mode 100644 index 0000000..b822d67 --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinNavigator.kt @@ -0,0 +1,97 @@ +package com.ejada.hmg.penguin + +import android.content.Context +import com.google.gson.Gson +import com.peng.pennavmap.PlugAndPlaySDK +import com.peng.pennavmap.connections.ApiController +import com.peng.pennavmap.interfaces.RefIdDelegate +import com.peng.pennavmap.models.TokenModel +import com.peng.pennavmap.models.postmodels.PostToken +import com.peng.pennavmap.utils.AppSharedData +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import android.util.Log + + +class PenguinNavigator() { + + fun navigateTo(mContext: Context, refID: String, delegate: RefIdDelegate,clientID : String,clientKey : String ) { + val postToken = PostToken(clientID, clientKey) + getToken(mContext, postToken, object : RefIdDelegate { + override fun onRefByIDSuccess(PoiId: String?) { + Log.e("navigateTo", "PoiId is+++++++ $PoiId") + + PlugAndPlaySDK.navigateTo(mContext, refID, object : RefIdDelegate { + override fun onRefByIDSuccess(PoiId: String?) { + Log.e("navigateTo", "PoiId 2is+++++++ $PoiId") + + delegate.onRefByIDSuccess(refID) + + } + + override fun onGetByRefIDError(error: String?) { + delegate.onRefByIDSuccess(error) + } + + }) + + + } + + override fun onGetByRefIDError(error: String?) { + delegate.onRefByIDSuccess(error) + } + + }) + + } + + fun getToken(mContext: Context, postToken: PostToken?, apiTokenCallBack: RefIdDelegate) { + try { + // Create the API call + val purposesCall: Call = ApiController.getInstance(mContext) + .apiMethods + .getToken(postToken) + + // Enqueue the call for asynchronous execution + purposesCall.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful() && response.body() != null) { + try { + response.body()?.use { responseBody -> + val responseBodyString: String = responseBody.string() // Use `string()` to get the actual response content + if (responseBodyString.isNotEmpty()) { + val tokenModel = Gson().fromJson(responseBodyString, TokenModel::class.java) + if (tokenModel != null && tokenModel.token != null) { + AppSharedData.apiToken = tokenModel.token + apiTokenCallBack.onRefByIDSuccess(tokenModel.token) + } else { + apiTokenCallBack.onGetByRefIDError("Failed to parse token model") + } + } else { + apiTokenCallBack.onGetByRefIDError("Response body is empty") + } + } + } catch (e: Exception) { + apiTokenCallBack.onGetByRefIDError("An error occurred: ${e.message}") + } + } else { + apiTokenCallBack.onGetByRefIDError("Unsuccessful response: " + response.code()) + } + } + + override fun onFailure(call: Call, t: Throwable) { + apiTokenCallBack.onGetByRefIDError(t.message) + } + }) + } catch (error: Exception) { + apiTokenCallBack.onGetByRefIDError("Exception during API call: $error") + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinView.kt b/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinView.kt new file mode 100644 index 0000000..6c7306d --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/penguin/PenguinView.kt @@ -0,0 +1,376 @@ +package com.ejada.hmg.penguin + +import android.app.Activity +import android.content.Context +import android.content.Context.RECEIVER_EXPORTED +import android.content.IntentFilter +import android.graphics.Color +import android.os.Build +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import android.widget.Toast +import androidx.annotation.RequiresApi +import com.ejada.hmg.PermissionManager.PermissionManager +import com.ejada.hmg.PermissionManager.PermissionResultReceiver +import com.ejada.hmg.MainActivity +import com.ejada.hmg.PermissionManager.PermissionHelper +import com.peng.pennavmap.PlugAndPlayConfiguration +import com.peng.pennavmap.PlugAndPlaySDK +import com.peng.pennavmap.enums.InitializationErrorType +import com.peng.pennavmap.interfaces.PenNavUIDelegate +import com.peng.pennavmap.utils.Languages +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.platform.PlatformView +import com.ejada.hmg.penguin.PenguinNavigator +import com.peng.pennavmap.interfaces.PIEventsDelegate +import com.peng.pennavmap.interfaces.PILocationDelegate +import com.peng.pennavmap.interfaces.RefIdDelegate +import com.peng.pennavmap.models.LocationMessage +import com.peng.pennavmap.models.PIReportIssue +import java.util.ArrayList +import penguin.com.pennav.renderer.PIRendererSettings + +/** + * Custom PlatformView for displaying Penguin UI components within a Flutter app. + * Implements `PlatformView` for rendering the view, `MethodChannel.MethodCallHandler` for handling method calls, + * and `PenNavUIDelegate` for handling SDK events. + */ +@RequiresApi(Build.VERSION_CODES.O) +internal class PenguinView( + context: Context, + id: Int, + val creationParams: Map, + messenger: BinaryMessenger, + activity: MainActivity, + val channel: MethodChannel +) : PlatformView, MethodChannel.MethodCallHandler, PenNavUIDelegate, PIEventsDelegate, + PILocationDelegate { + // The layout for displaying the Penguin UI + private val mapLayout: RelativeLayout = RelativeLayout(context) + private val _context: Context = context + + private val permissionResultReceiver: PermissionResultReceiver + private val permissionIntentFilter = IntentFilter("PERMISSION_RESULT_ACTION") + + private companion object { + const val PERMISSIONS_REQUEST_CODE = 1 + } + + private lateinit var permissionManager: PermissionManager + + // Reference to the main activity + private var _activity: Activity = activity + + private lateinit var mContext: Context + + lateinit var navigator: PenguinNavigator + + init { + // Set layout parameters for the mapLayout + mapLayout.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + mContext = context + + + permissionResultReceiver = PermissionResultReceiver { granted -> + if (granted) { + onPermissionsGranted() + } else { + onPermissionsDenied() + } + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mContext.registerReceiver( + permissionResultReceiver, + permissionIntentFilter, + RECEIVER_EXPORTED + ) + } else { + mContext.registerReceiver( + permissionResultReceiver, + permissionIntentFilter, + ) + } + + // Set the background color of the layout + mapLayout.setBackgroundColor(Color.RED) + + permissionManager = PermissionManager( + context = mContext, + listener = object : PermissionManager.PermissionListener { + override fun onPermissionGranted() { + // Handle permissions granted + onPermissionsGranted() + } + + override fun onPermissionDenied() { + // Handle permissions denied + onPermissionsDenied() + } + }, + requestCode = PERMISSIONS_REQUEST_CODE, + PermissionHelper.getRequiredPermissions().get(0) + ) + + if (!permissionManager.arePermissionsGranted()) { + permissionManager.requestPermissions(_activity) + } else { + // Permissions already granted + permissionManager.listener.onPermissionGranted() + } + + + } + + private fun onPermissionsGranted() { + // Handle the actions when permissions are granted + Log.d("PermissionsResult", "onPermissionsGranted") + // Register the platform view factory for creating custom views + + // Initialize the Penguin SDK + initPenguin() + + + } + + private fun onPermissionsDenied() { + // Handle the actions when permissions are denied + Log.d("PermissionsResult", "onPermissionsDenied") + + } + + /** + * Returns the view associated with this PlatformView. + * + * @return The main view for this PlatformView. + */ + override fun getView(): View { + return mapLayout + } + + /** + * Cleans up resources associated with this PlatformView. + */ + override fun dispose() { + // Cleanup code if needed + } + + /** + * Handles method calls from Dart code. + * + * @param call The method call from Dart. + * @param result The result callback to send responses back to Dart. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + // Handle method calls from Dart code here + } + + /** + * Initializes the Penguin SDK with custom configuration and delegates. + */ + private fun initPenguin() { + navigator = PenguinNavigator() + // Configure the PlugAndPlaySDK + val language = when (creationParams["languageCode"] as String) { + "ar" -> Languages.ar + "en" -> Languages.en + else -> { + Languages.en + } + } + + +// PlugAndPlaySDK.configuration = Builder() +// .setClientData(MConstantsDemo.CLIENT_ID, MConstantsDemo.CLIENT_KEY) +// .setLanguageID(selectedLanguage) +// .setBaseUrl(MConstantsDemo.DATA_URL, MConstantsDemo.POSITION_URL) +// .setServiceName(MConstantsDemo.DATA_SERVICE_NAME, MConstantsDemo.POSITION_SERVICE_NAME) +// .setUserName(name) +// .setSimulationModeEnabled(isSimulation) +// .setCustomizeColor(if (MConstantsDemo.APP_COLOR != null) MConstantsDemo.APP_COLOR else "#2CA0AF") +// .setEnableBackButton(MConstantsDemo.SHOW_BACK_BUTTON) +// .setCampusId(MConstantsDemo.selectedCampusId) +// +// .setShowUILoader(true) +// .build() + + PIRendererSettings.styleUri = "mapbox://styles/rwaid/cm3h30b36007v01qz7ik8a0sk" + + PlugAndPlaySDK.configuration = PlugAndPlayConfiguration.Builder() + .setBaseUrl( + creationParams["dataURL"] as String, + creationParams["positionURL"] as String + ) + .setServiceName( + creationParams["dataServiceName"] as String, + creationParams["positionServiceName"] as String + ) + .setClientData( + creationParams["clientID"] as String, + creationParams["clientKey"] as String + ) + .setUserName(creationParams["username"] as String) +// .setLanguageID(Languages.en) + .setLanguageID(language) + .setSimulationModeEnabled(creationParams["isSimulationModeEnabled"] as Boolean) + .setEnableBackButton(true) +// .setDeepLinkData("deeplink") + .setCustomizeColor("#2CA0AF") + .setDeepLinkSchema("", "") + .setIsEnableReportIssue(true) + .setDeepLinkData("") + .setEnableSharedLocationCallBack(false) + .setShowUILoader(true) + .setCampusId(creationParams["projectID"] as Int) + .build() + + + Log.d( + "TAG", + "initPenguin: ${creationParams["projectID"]}" + ) + + Log.d( + "TAG", + "initPenguin: creation param are ${creationParams}" + ) + + // Set location delegate to handle location updates +// PlugAndPlaySDK.setPiLocationDelegate { + // Example code to handle location updates + // Uncomment and modify as needed + // if (location.size() > 0) + // Toast.makeText(_context, "Location Info Latitude: ${location[0]}, Longitude: ${location[1]}", Toast.LENGTH_SHORT).show() +// } + + // Set events delegate for reporting issues +// PlugAndPlaySDK.setPiEventsDelegate(new PIEventsDelegate() { +// @Override +// public void onReportIssue(PIReportIssue issue) { +// Log.e("Issue Reported: ", issue.getReportType()); +// } +// // Implement issue reporting logic here } +// @Override +// public void onSharedLocation(String link) { +// // Implement Shared location logic here +// } +// }) + + // Start the Penguin SDK + PlugAndPlaySDK.setPiEventsDelegate(this) + PlugAndPlaySDK.setPiLocationDelegate(this) + PlugAndPlaySDK.start(mContext, this) + } + + + /** + * Navigates to the specified reference ID. + * + * @param refID The reference ID to navigate to. + */ + fun navigateTo(refID: String) { + try { + if (refID.isBlank()) { + Log.e("navigateTo", "Invalid refID: The reference ID is blank.") + } +// referenceId = refID + navigator.navigateTo(mContext, refID,object : RefIdDelegate { + override fun onRefByIDSuccess(PoiId: String?) { + Log.e("navigateTo", "PoiId is penguin view+++++++ $PoiId") + +// channelFlutter.invokeMethod( +// PenguinMethod.navigateToPOI.name, +// "navigateTo Success" +// ) + } + + override fun onGetByRefIDError(error: String?) { + Log.e("navigateTo", "error is penguin view+++++++ $error") + +// channelFlutter.invokeMethod( +// PenguinMethod.navigateToPOI.name, +// "navigateTo Failed: Invalid refID" +// ) + } + } , creationParams["clientID"] as String, creationParams["clientKey"] as String ) + + } catch (e: Exception) { + Log.e("navigateTo", "Exception occurred during navigation: ${e.message}", e) +// channelFlutter.invokeMethod( +// PenguinMethod.navigateToPOI.name, +// "Failed: Exception - ${e.message}" +// ) + } + } + + /** + * Called when Penguin UI setup is successful. + * + * @param warningCode Optional warning code received from the SDK. + */ + override fun onPenNavSuccess(warningCode: String?) { + val clinicId = creationParams["clinicID"] as String + + if(clinicId.isEmpty()) return + + navigateTo(clinicId) + } + + /** + * Called when there is an initialization error with Penguin UI. + * + * @param description Description of the error. + * @param errorType Type of initialization error. + */ + override fun onPenNavInitializationError( + description: String?, + errorType: InitializationErrorType? + ) { + val arguments: Map = mapOf( + "description" to description, + "type" to errorType?.name + ) + Log.d( + "description", + "description : ${description}" + ) + + channel.invokeMethod(PenguinMethod.onPenNavInitializationError.name, arguments) + Toast.makeText(mContext, "Navigation Error: $description", Toast.LENGTH_SHORT).show() + } + + /** + * Called when Penguin UI is dismissed. + */ + override fun onPenNavUIDismiss() { + // Handle UI dismissal if needed + try { + mContext.unregisterReceiver(permissionResultReceiver) + dispose(); + } catch (e: IllegalArgumentException) { + Log.e("PenguinView", "Receiver not registered: $e") + } + } + + override fun onReportIssue(issue: PIReportIssue?) { + TODO("Not yet implemented") + } + + override fun onSharedLocation(link: String?) { + TODO("Not yet implemented") + } + + override fun onLocationOffCampus(location: ArrayList?) { + TODO("Not yet implemented") + } + + override fun onLocationMessage(locationMessage: LocationMessage?) { + TODO("Not yet implemented") + } +} diff --git a/android/app/src/main/res/values/mapbox_access_token.xml b/android/app/src/main/res/values/mapbox_access_token.xml index f1daf69..65bc4b3 100644 --- a/android/app/src/main/res/values/mapbox_access_token.xml +++ b/android/app/src/main/res/values/mapbox_access_token.xml @@ -1,3 +1,3 @@ - sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 6c4ac3d..2d10333 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -19,5 +19,5 @@ Geofence requests happened too frequently. - sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg + pk.eyJ1IjoicndhaWQiLCJhIjoiY2x5cGo4aHNjMGNsbTJyc2djempobGQxaSJ9.RCaC6WrUt4A4YnZNfxnONQ diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 866b270..9b679bc 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ab39a10..6d0842d 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,7 +18,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.3" apply false +// id("com.android.application") version "8.9.3" apply false + id("com.android.application") version "8.9.3" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/assets/images/png/bmi_image_1.png b/assets/images/png/bmi_image_1.png new file mode 100644 index 0000000..db3a613 Binary files /dev/null and b/assets/images/png/bmi_image_1.png differ diff --git a/assets/images/png/home_health_care.png b/assets/images/png/home_health_care.png new file mode 100644 index 0000000..21378c4 Binary files /dev/null and b/assets/images/png/home_health_care.png differ diff --git a/assets/images/png/pharmacy_service.png b/assets/images/png/pharmacy_service.png new file mode 100644 index 0000000..7093d41 Binary files /dev/null and b/assets/images/png/pharmacy_service.png differ diff --git a/assets/images/png/smartwatches/Apple-Watch-6.png b/assets/images/png/smartwatches/Apple-Watch-6.png new file mode 100644 index 0000000..1e67050 Binary files /dev/null and b/assets/images/png/smartwatches/Apple-Watch-6.png differ diff --git a/assets/images/png/smartwatches/apple-watch-1.jpeg b/assets/images/png/smartwatches/apple-watch-1.jpeg new file mode 100644 index 0000000..7262e7e Binary files /dev/null and b/assets/images/png/smartwatches/apple-watch-1.jpeg differ diff --git a/assets/images/png/smartwatches/apple-watch-2.jpg b/assets/images/png/smartwatches/apple-watch-2.jpg new file mode 100644 index 0000000..f688f74 Binary files /dev/null and b/assets/images/png/smartwatches/apple-watch-2.jpg differ diff --git a/assets/images/png/smartwatches/apple-watch-3.jpg b/assets/images/png/smartwatches/apple-watch-3.jpg new file mode 100644 index 0000000..b68c1ce Binary files /dev/null and b/assets/images/png/smartwatches/apple-watch-3.jpg differ diff --git a/assets/images/png/smartwatches/apple-watch-4.jpg b/assets/images/png/smartwatches/apple-watch-4.jpg new file mode 100644 index 0000000..2fc19b6 Binary files /dev/null and b/assets/images/png/smartwatches/apple-watch-4.jpg differ diff --git a/assets/images/png/smartwatches/apple-watch-5.jpg b/assets/images/png/smartwatches/apple-watch-5.jpg new file mode 100644 index 0000000..4c497ea Binary files /dev/null and b/assets/images/png/smartwatches/apple-watch-5.jpg differ diff --git a/assets/images/png/smartwatches/bloodoxygen_icon.svg b/assets/images/png/smartwatches/bloodoxygen_icon.svg new file mode 100644 index 0000000..0971a30 --- /dev/null +++ b/assets/images/png/smartwatches/bloodoxygen_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/png/smartwatches/calories_icon.svg b/assets/images/png/smartwatches/calories_icon.svg new file mode 100644 index 0000000..660ce0d --- /dev/null +++ b/assets/images/png/smartwatches/calories_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/png/smartwatches/distance_icon.svg b/assets/images/png/smartwatches/distance_icon.svg new file mode 100644 index 0000000..29dcf3d --- /dev/null +++ b/assets/images/png/smartwatches/distance_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/png/smartwatches/galaxy_fit_3.jpg b/assets/images/png/smartwatches/galaxy_fit_3.jpg new file mode 100644 index 0000000..ff05834 Binary files /dev/null and b/assets/images/png/smartwatches/galaxy_fit_3.jpg differ diff --git a/assets/images/png/smartwatches/galaxy_watch_7.webp b/assets/images/png/smartwatches/galaxy_watch_7.webp new file mode 100644 index 0000000..09748b4 Binary files /dev/null and b/assets/images/png/smartwatches/galaxy_watch_7.webp differ diff --git a/assets/images/png/smartwatches/galaxy_watch_7_classic.jpg b/assets/images/png/smartwatches/galaxy_watch_7_classic.jpg new file mode 100644 index 0000000..f177dd4 Binary files /dev/null and b/assets/images/png/smartwatches/galaxy_watch_7_classic.jpg differ diff --git a/assets/images/png/smartwatches/galaxy_watch_8.jpg b/assets/images/png/smartwatches/galaxy_watch_8.jpg new file mode 100644 index 0000000..7fd4746 Binary files /dev/null and b/assets/images/png/smartwatches/galaxy_watch_8.jpg differ diff --git a/assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg b/assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg new file mode 100644 index 0000000..6e84096 Binary files /dev/null and b/assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg differ diff --git a/assets/images/png/smartwatches/galaxy_watch_ultra.jpg b/assets/images/png/smartwatches/galaxy_watch_ultra.jpg new file mode 100644 index 0000000..e401d73 Binary files /dev/null and b/assets/images/png/smartwatches/galaxy_watch_ultra.jpg differ diff --git a/assets/images/png/smartwatches/heartrate_icon.svg b/assets/images/png/smartwatches/heartrate_icon.svg new file mode 100644 index 0000000..dac05ef --- /dev/null +++ b/assets/images/png/smartwatches/heartrate_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/png/smartwatches/steps_icon.svg b/assets/images/png/smartwatches/steps_icon.svg new file mode 100644 index 0000000..4af073a --- /dev/null +++ b/assets/images/png/smartwatches/steps_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/E_Referral.svg b/assets/images/svg/E_Referral.svg new file mode 100644 index 0000000..fb6b859 --- /dev/null +++ b/assets/images/svg/E_Referral.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/activity.svg b/assets/images/svg/activity.svg new file mode 100644 index 0000000..7e1c342 --- /dev/null +++ b/assets/images/svg/activity.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/add_icon_dark.svg b/assets/images/svg/add_icon_dark.svg new file mode 100644 index 0000000..399df3c --- /dev/null +++ b/assets/images/svg/add_icon_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/age_icon.svg b/assets/images/svg/age_icon.svg new file mode 100644 index 0000000..8acfad3 --- /dev/null +++ b/assets/images/svg/age_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/ancillary_orders_list_icon.svg b/assets/images/svg/ancillary_orders_list_icon.svg new file mode 100644 index 0000000..f0497d8 --- /dev/null +++ b/assets/images/svg/ancillary_orders_list_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/approximate_ovulation_accordion.svg b/assets/images/svg/approximate_ovulation_accordion.svg new file mode 100644 index 0000000..a721d61 --- /dev/null +++ b/assets/images/svg/approximate_ovulation_accordion.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/ask_doctor_medical_file_icon.svg b/assets/images/svg/ask_doctor_medical_file_icon.svg new file mode 100644 index 0000000..11facfb --- /dev/null +++ b/assets/images/svg/ask_doctor_medical_file_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/blood_pressure.svg b/assets/images/svg/blood_pressure.svg new file mode 100644 index 0000000..67badbe --- /dev/null +++ b/assets/images/svg/blood_pressure.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/blood_pressure_icon.svg b/assets/images/svg/blood_pressure_icon.svg new file mode 100644 index 0000000..0b027ad --- /dev/null +++ b/assets/images/svg/blood_pressure_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/blood_sugar_icon.svg b/assets/images/svg/blood_sugar_icon.svg new file mode 100644 index 0000000..3c77019 --- /dev/null +++ b/assets/images/svg/blood_sugar_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/blood_sugar_only_icon.svg b/assets/images/svg/blood_sugar_only_icon.svg new file mode 100644 index 0000000..f81cee8 --- /dev/null +++ b/assets/images/svg/blood_sugar_only_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/blood_type.svg b/assets/images/svg/blood_type.svg new file mode 100644 index 0000000..5aded31 --- /dev/null +++ b/assets/images/svg/blood_type.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bloodcholestrol.svg b/assets/images/svg/bloodcholestrol.svg new file mode 100644 index 0000000..8a77bb5 --- /dev/null +++ b/assets/images/svg/bloodcholestrol.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/bloodsugar.svg b/assets/images/svg/bloodsugar.svg new file mode 100644 index 0000000..a97032c --- /dev/null +++ b/assets/images/svg/bloodsugar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bmi.svg b/assets/images/svg/bmi.svg new file mode 100644 index 0000000..7ee99db --- /dev/null +++ b/assets/images/svg/bmi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/bmi_2.svg b/assets/images/svg/bmi_2.svg new file mode 100644 index 0000000..38468d7 --- /dev/null +++ b/assets/images/svg/bmi_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bmr.svg b/assets/images/svg/bmr.svg new file mode 100644 index 0000000..6b797e4 --- /dev/null +++ b/assets/images/svg/bmr.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bulb.svg b/assets/images/svg/bulb.svg new file mode 100644 index 0000000..94553a5 --- /dev/null +++ b/assets/images/svg/bulb.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/calories.svg b/assets/images/svg/calories.svg new file mode 100644 index 0000000..9f8d2b5 --- /dev/null +++ b/assets/images/svg/calories.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/covid_19.svg b/assets/images/svg/covid_19.svg new file mode 100644 index 0000000..f3aa128 --- /dev/null +++ b/assets/images/svg/covid_19.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/cup_add.svg b/assets/images/svg/cup_add.svg new file mode 100644 index 0000000..ebe186a --- /dev/null +++ b/assets/images/svg/cup_add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/cup_empty.svg b/assets/images/svg/cup_empty.svg new file mode 100644 index 0000000..fae08fe --- /dev/null +++ b/assets/images/svg/cup_empty.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/cup_filled.svg b/assets/images/svg/cup_filled.svg new file mode 100644 index 0000000..6a085bb --- /dev/null +++ b/assets/images/svg/cup_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/daily_water_monitor.svg b/assets/images/svg/daily_water_monitor.svg new file mode 100644 index 0000000..b5f057d --- /dev/null +++ b/assets/images/svg/daily_water_monitor.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/due_date_accordion.svg b/assets/images/svg/due_date_accordion.svg new file mode 100644 index 0000000..828a1c9 --- /dev/null +++ b/assets/images/svg/due_date_accordion.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/dumbell_icon.svg b/assets/images/svg/dumbell_icon.svg new file mode 100644 index 0000000..1d6db5f --- /dev/null +++ b/assets/images/svg/dumbell_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/fertile_window_accordion.svg b/assets/images/svg/fertile_window_accordion.svg new file mode 100644 index 0000000..63f0173 --- /dev/null +++ b/assets/images/svg/fertile_window_accordion.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/svg/file.svg b/assets/images/svg/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/assets/images/svg/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/svg/files.svg b/assets/images/svg/files.svg new file mode 100644 index 0000000..dfc862e --- /dev/null +++ b/assets/images/svg/files.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/svg/gallery.svg b/assets/images/svg/gallery.svg new file mode 100644 index 0000000..ec1c45e --- /dev/null +++ b/assets/images/svg/gallery.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/images/svg/genderInputIcon.svg b/assets/images/svg/genderInputIcon.svg new file mode 100644 index 0000000..4482ae3 --- /dev/null +++ b/assets/images/svg/genderInputIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/gender_icon.svg b/assets/images/svg/gender_icon.svg new file mode 100644 index 0000000..4573125 --- /dev/null +++ b/assets/images/svg/gender_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/general_health.svg b/assets/images/svg/general_health.svg new file mode 100644 index 0000000..0102df1 --- /dev/null +++ b/assets/images/svg/general_health.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/glass_icon.svg b/assets/images/svg/glass_icon.svg new file mode 100644 index 0000000..1df8eec --- /dev/null +++ b/assets/images/svg/glass_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/graph_icon.svg b/assets/images/svg/graph_icon.svg new file mode 100644 index 0000000..7bb6fbb --- /dev/null +++ b/assets/images/svg/graph_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/green_tick_icon.svg b/assets/images/svg/green_tick_icon.svg new file mode 100644 index 0000000..e041191 --- /dev/null +++ b/assets/images/svg/green_tick_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/health_calculators_services_icon.svg b/assets/images/svg/health_calculators_services_icon.svg new file mode 100644 index 0000000..9f30d08 --- /dev/null +++ b/assets/images/svg/health_calculators_services_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/health_converters_icon.svg b/assets/images/svg/health_converters_icon.svg new file mode 100644 index 0000000..225ad01 --- /dev/null +++ b/assets/images/svg/health_converters_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/heart_rate.svg b/assets/images/svg/heart_rate.svg new file mode 100644 index 0000000..15c754f --- /dev/null +++ b/assets/images/svg/heart_rate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/height.svg b/assets/images/svg/height.svg new file mode 100644 index 0000000..f275d34 --- /dev/null +++ b/assets/images/svg/height.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/height_2.svg b/assets/images/svg/height_2.svg new file mode 100644 index 0000000..a1c361a --- /dev/null +++ b/assets/images/svg/height_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/height_icon.svg b/assets/images/svg/height_icon.svg new file mode 100644 index 0000000..78cefdc --- /dev/null +++ b/assets/images/svg/height_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/ibw.svg b/assets/images/svg/ibw.svg new file mode 100644 index 0000000..2f11ca4 --- /dev/null +++ b/assets/images/svg/ibw.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/insurance_approval_icon.svg b/assets/images/svg/insurance_approval_icon.svg new file mode 100644 index 0000000..b46a54a --- /dev/null +++ b/assets/images/svg/insurance_approval_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/internet_pairing_icon.svg b/assets/images/svg/internet_pairing_icon.svg new file mode 100644 index 0000000..3e1ac63 --- /dev/null +++ b/assets/images/svg/internet_pairing_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/invoices_list_icon.svg b/assets/images/svg/invoices_list_icon.svg new file mode 100644 index 0000000..f123096 --- /dev/null +++ b/assets/images/svg/invoices_list_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/list_icon.svg b/assets/images/svg/list_icon.svg new file mode 100644 index 0000000..e68f20b --- /dev/null +++ b/assets/images/svg/list_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/low_indicator_icon.svg b/assets/images/svg/low_indicator_icon.svg new file mode 100644 index 0000000..f2ca09f --- /dev/null +++ b/assets/images/svg/low_indicator_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/medical_reports_icon.svg b/assets/images/svg/medical_reports_icon.svg new file mode 100644 index 0000000..862b813 --- /dev/null +++ b/assets/images/svg/medical_reports_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/minimize_icon.svg b/assets/images/svg/minimize_icon.svg new file mode 100644 index 0000000..b60a041 --- /dev/null +++ b/assets/images/svg/minimize_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/monthly_reports_icon.svg b/assets/images/svg/monthly_reports_icon.svg new file mode 100644 index 0000000..5e786e2 --- /dev/null +++ b/assets/images/svg/monthly_reports_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/my_doctors_icon.svg b/assets/images/svg/my_doctors_icon.svg new file mode 100644 index 0000000..c5fc541 --- /dev/null +++ b/assets/images/svg/my_doctors_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/my_radiology_icon.svg b/assets/images/svg/my_radiology_icon.svg new file mode 100644 index 0000000..7b5ebe4 --- /dev/null +++ b/assets/images/svg/my_radiology_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/my_sick_leave_icon.svg b/assets/images/svg/my_sick_leave_icon.svg new file mode 100644 index 0000000..f488cff --- /dev/null +++ b/assets/images/svg/my_sick_leave_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/next_period_accordion.svg b/assets/images/svg/next_period_accordion.svg new file mode 100644 index 0000000..4ff65db --- /dev/null +++ b/assets/images/svg/next_period_accordion.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/normal_status_green_icon.svg b/assets/images/svg/normal_status_green_icon.svg new file mode 100644 index 0000000..b3f2619 --- /dev/null +++ b/assets/images/svg/normal_status_green_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/notification_icon_grey.svg b/assets/images/svg/notification_icon_grey.svg new file mode 100644 index 0000000..9e5e8d5 --- /dev/null +++ b/assets/images/svg/notification_icon_grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/open_camera.svg b/assets/images/svg/open_camera.svg new file mode 100644 index 0000000..171452a --- /dev/null +++ b/assets/images/svg/open_camera.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/outer_bubbles.svg b/assets/images/svg/outer_bubbles.svg new file mode 100644 index 0000000..cfe860d --- /dev/null +++ b/assets/images/svg/outer_bubbles.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/phramacy_icon.svg b/assets/images/svg/phramacy_icon.svg new file mode 100644 index 0000000..a9c5d1c --- /dev/null +++ b/assets/images/svg/phramacy_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/pregnancy_test_day_accordion.svg b/assets/images/svg/pregnancy_test_day_accordion.svg new file mode 100644 index 0000000..5a29588 --- /dev/null +++ b/assets/images/svg/pregnancy_test_day_accordion.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/svg/profile_icon.svg b/assets/images/svg/profile_icon.svg new file mode 100644 index 0000000..20dfb2b --- /dev/null +++ b/assets/images/svg/profile_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/rate_1.svg b/assets/images/svg/rate_1.svg new file mode 100644 index 0000000..8e1c2f2 --- /dev/null +++ b/assets/images/svg/rate_1.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/rate_2.svg b/assets/images/svg/rate_2.svg new file mode 100644 index 0000000..9500556 --- /dev/null +++ b/assets/images/svg/rate_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/rate_3.svg b/assets/images/svg/rate_3.svg new file mode 100644 index 0000000..7c9d0f5 --- /dev/null +++ b/assets/images/svg/rate_3.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/rate_4.svg b/assets/images/svg/rate_4.svg new file mode 100644 index 0000000..7f82267 --- /dev/null +++ b/assets/images/svg/rate_4.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/rate_5.svg b/assets/images/svg/rate_5.svg new file mode 100644 index 0000000..a7308ba --- /dev/null +++ b/assets/images/svg/rate_5.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/resp_rate.svg b/assets/images/svg/resp_rate.svg new file mode 100644 index 0000000..7038793 --- /dev/null +++ b/assets/images/svg/resp_rate.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/send_email_icon.svg b/assets/images/svg/send_email_icon.svg new file mode 100644 index 0000000..eb8684a --- /dev/null +++ b/assets/images/svg/send_email_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/sick_leave_report_icon.svg b/assets/images/svg/sick_leave_report_icon.svg new file mode 100644 index 0000000..521c063 --- /dev/null +++ b/assets/images/svg/sick_leave_report_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/smartwatch_icon.svg b/assets/images/svg/smartwatch_icon.svg new file mode 100644 index 0000000..162ab36 --- /dev/null +++ b/assets/images/svg/smartwatch_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/switch.svg b/assets/images/svg/switch.svg new file mode 100644 index 0000000..1db8753 --- /dev/null +++ b/assets/images/svg/switch.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/symptom_bottom_icon.svg b/assets/images/svg/symptom_bottom_icon.svg new file mode 100644 index 0000000..bc72971 --- /dev/null +++ b/assets/images/svg/symptom_bottom_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/symptom_checker_icon.svg b/assets/images/svg/symptom_checker_icon.svg new file mode 100644 index 0000000..e41c1dd --- /dev/null +++ b/assets/images/svg/symptom_checker_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/temperature.svg b/assets/images/svg/temperature.svg new file mode 100644 index 0000000..14c7da4 --- /dev/null +++ b/assets/images/svg/temperature.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/trade_down_red.svg b/assets/images/svg/trade_down_red.svg new file mode 100644 index 0000000..7c77c8e --- /dev/null +++ b/assets/images/svg/trade_down_red.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/trade_down_yellow.svg b/assets/images/svg/trade_down_yellow.svg new file mode 100644 index 0000000..93c6805 --- /dev/null +++ b/assets/images/svg/trade_down_yellow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/triglycerides.svg b/assets/images/svg/triglycerides.svg new file mode 100644 index 0000000..0f8facd --- /dev/null +++ b/assets/images/svg/triglycerides.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/update_insurance_icon.svg b/assets/images/svg/update_insurance_icon.svg new file mode 100644 index 0000000..684d672 --- /dev/null +++ b/assets/images/svg/update_insurance_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/water_bottle.svg b/assets/images/svg/water_bottle.svg new file mode 100644 index 0000000..4763d7e --- /dev/null +++ b/assets/images/svg/water_bottle.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/svg/weight.svg b/assets/images/svg/weight.svg new file mode 100644 index 0000000..6c42c17 --- /dev/null +++ b/assets/images/svg/weight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/weight_2.svg b/assets/images/svg/weight_2.svg new file mode 100644 index 0000000..c22441f --- /dev/null +++ b/assets/images/svg/weight_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight_icon.svg b/assets/images/svg/weight_icon.svg new file mode 100644 index 0000000..f93c662 --- /dev/null +++ b/assets/images/svg/weight_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight_scale_icon.svg b/assets/images/svg/weight_scale_icon.svg new file mode 100644 index 0000000..c3329ff --- /dev/null +++ b/assets/images/svg/weight_scale_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight_tracker_icon.svg b/assets/images/svg/weight_tracker_icon.svg new file mode 100644 index 0000000..5110575 --- /dev/null +++ b/assets/images/svg/weight_tracker_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/women_health.svg b/assets/images/svg/women_health.svg new file mode 100644 index 0000000..5eca669 --- /dev/null +++ b/assets/images/svg/women_health.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/yellow_arrow_down_icon.svg b/assets/images/svg/yellow_arrow_down_icon.svg new file mode 100644 index 0000000..f2ca09f --- /dev/null +++ b/assets/images/svg/yellow_arrow_down_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 787a718..b8fc7eb 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -876,5 +876,6 @@ "endDate": "تاريخ الانتهاء", "walkin": "زيارة بدون موعد", "laserClinic": "عيادة الليزر", - "continueString": "يكمل" + "continueString": "يكمل", + "covid_info": "تجري مستشفيات د. سليمان الحبيب فحص فيروس كورونا المستجد وتصدر شهادات السفر على مدار الساعة، طوال أيام الأسبوع، وبسرعة ودقة عالية. يمكن للراغبين في الاستفادة من هذه الخدمة زيارة أحد فروع مستشفيات د. سليمان الحبيب وإجراء فحص كورونا خلال بضع دقائق والحصول على النتائج خلال عدة ساعات خدمة فحص فيروس كورونا Covid 19 بتقنية PCR للكشف عن الفيروس وفقاً لأعلى المعايير العالمية وبأحدث أجهزة RT-PCR عالية الدقة (GeneXpert الأمريكي وغيره)، وهي طرق معتمدة من قبل هيئة الغذاء والدواء وكذلك من قبل المركز السعودي للوقاية من الأمراض المُعدية" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 7b8c4b8..7839083 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -872,6 +872,6 @@ "searchClinic": "Search Clinic", "walkin": "Walk In", "continueString": "Continue", - "laserClinic": "Laser Clinic" - + "laserClinic": "Laser Clinic", + "covid_info" :"Dr. Sulaiman Al Habib hospitals are conducting a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes, and obtain the result within several hours. Corona Virus Covid 19 testing service with PCR technology to detect the virus according to the highest international standards and with the latest high-precision RT-PCR devices (American GeneXpert and others), That is approved by the Food and Drug Authority as well as by the Saudi Center for Infectious Diseases Prevention." } \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/ios/Controllers/MainFlutterVC.swift b/ios/Controllers/MainFlutterVC.swift new file mode 100644 index 0000000..4f91d05 --- /dev/null +++ b/ios/Controllers/MainFlutterVC.swift @@ -0,0 +1,118 @@ +// +// MainFlutterVC.swift +// Runner +// +// Created by ZiKambrani on 25/03/1442 AH. +// + +import UIKit +import Flutter +import NetworkExtension +import SystemConfiguration.CaptiveNetwork + +class MainFlutterVC: FlutterViewController { + + override func viewDidLoad() { + super.viewDidLoad() + +// flutterMethodChannel?.setMethodCallHandler { (methodCall, result) in +// +// if methodCall.method == "connectHMGInternetWifi"{ +// self.connectHMGInternetWifi(methodCall:methodCall, result: result) +// +// }else if methodCall.method == "connectHMGGuestWifi"{ +// self.connectHMGGuestWifi(methodCall:methodCall, result: result) +// +// }else if methodCall.method == "isHMGNetworkAvailable"{ +// self.isHMGNetworkAvailable(methodCall:methodCall, result: result) +// +// }else if methodCall.method == "registerHmgGeofences"{ +// self.registerHmgGeofences(result: result) +// } +// +// print("") +// } +// +// FlutterText.with(key: "errorConnectingHmgNetwork") { (localized) in +// print(localized) +// } + + } + + + // Connect HMG Wifi and Internet + func connectHMGInternetWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + + guard let pateintId = (methodCall.arguments as? [Any])?.first as? String + else { return assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") } + + + HMG_Internet.shared.connect(patientId: pateintId) { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + // Connect HMG-Guest for App Access + func connectHMGGuestWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + HMG_GUEST.shared.connect() { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + func isHMGNetworkAvailable(methodCall:FlutterMethodCall ,result: @escaping FlutterResult) -> Bool{ + guard let ssid = methodCall.arguments as? String else { + assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") + return false + } + + let queue = DispatchQueue.init(label: "com.hmg.wifilist") + NEHotspotHelper.register(options: nil, queue: queue) { (command) in + print(command) + + if(command.commandType == NEHotspotHelperCommandType.filterScanList) { + if let networkList = command.networkList{ + for network in networkList{ + print(network.ssid) + } + } + } + } + return false + + } + + + // Message Dailog + func showMessage(title:String, message:String){ + DispatchQueue.main.async { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert ) + alert.addAction(UIAlertAction(title: "OK", style: .destructive, handler: nil)) + self.present(alert, animated: true) { + + } + } + } + + // Register Geofence + func registerHmgGeofences(result: @escaping FlutterResult){ + flutterMethodChannel?.invokeMethod("getGeofencePreferenceKey", arguments: nil){ geoFencesJsonString in + if let jsonString = geoFencesJsonString as? String{ + let allZones = GeoZoneModel.list(from: jsonString) + HMG_Geofence().register(geoZones: allZones) + + }else{ + } + } + } + +} diff --git a/ios/Helper/API.swift b/ios/Helper/API.swift new file mode 100644 index 0000000..b487f03 --- /dev/null +++ b/ios/Helper/API.swift @@ -0,0 +1,22 @@ +// +// API.swift +// Runner +// +// Created by ZiKambrani on 04/04/1442 AH. +// + +import UIKit + +fileprivate let DOMAIN = "https://uat.hmgwebservices.com" +fileprivate let SERVICE = "Services/Patients.svc/REST" +fileprivate let BASE_URL = "\(DOMAIN)/\(SERVICE)" + +struct API { + static let WIFI_CREDENTIALS = "\(BASE_URL)/Hmg_SMS_Get_By_ProjectID_And_PatientID" +} + + +//struct API { +// static let WIFI_CREDENTIALS = FlutterConstants.WIFI_CREDENTIALS_URL +// static let LOG_GEOFENCE = FlutterConstants.LOG_GEOFENCE_URL +//} diff --git a/ios/Helper/Extensions.swift b/ios/Helper/Extensions.swift new file mode 100644 index 0000000..de67f9b --- /dev/null +++ b/ios/Helper/Extensions.swift @@ -0,0 +1,150 @@ +// +// Extensions.swift +// Runner +// +// Created by ZiKambrani on 04/04/1442 AH. +// + +import UIKit + + +extension String{ + func toUrl() -> URL?{ + return URL(string: self) + } + + func removeSpace() -> String?{ + return self.replacingOccurrences(of: " ", with: "") + } +} + +extension Date{ + func toString(format:String) -> String{ + let df = DateFormatter() + df.dateFormat = format + return df.string(from: self) + } +} + +extension Dictionary{ + func merge(dict:[String:Any?]) -> [String:Any?]{ + var self_ = self as! [String:Any?] + dict.forEach { (kv) in + self_.updateValue(kv.value, forKey: kv.key) + } + return self_ + } +} + +extension Bundle { + + func certificate(named name: String) -> SecCertificate { + let cerURL = self.url(forResource: name, withExtension: "cer")! + let cerData = try! Data(contentsOf: cerURL) + let cer = SecCertificateCreateWithData(nil, cerData as CFData)! + return cer + } + + func identity(named name: String, password: String) -> SecIdentity { + let p12URL = self.url(forResource: name, withExtension: "p12")! + let p12Data = try! Data(contentsOf: p12URL) + + var importedCF: CFArray? = nil + let options = [kSecImportExportPassphrase as String: password] + let err = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &importedCF) + precondition(err == errSecSuccess) + let imported = importedCF! as NSArray as! [[String:AnyObject]] + precondition(imported.count == 1) + + return (imported[0][kSecImportItemIdentity as String]!) as! SecIdentity + } + + +} + +extension SecCertificate{ + func trust() -> Bool?{ + var optionalTrust: SecTrust? + let policy = SecPolicyCreateBasicX509() + + let status = SecTrustCreateWithCertificates([self] as AnyObject, + policy, + &optionalTrust) + guard status == errSecSuccess else { return false} + let trust = optionalTrust! + + let stat = optionalTrust?.evaluateAllowing(rootCertificates: [self]) + return stat + } + + func secTrustObject() -> SecTrust?{ + var optionalTrust: SecTrust? + let policy = SecPolicyCreateBasicX509() + + let status = SecTrustCreateWithCertificates([self] as AnyObject, + policy, + &optionalTrust) + return optionalTrust + } +} + + +extension SecTrust { + + func evaluate() -> Bool { + var trustResult: SecTrustResultType = .invalid + let err = SecTrustEvaluate(self, &trustResult) + guard err == errSecSuccess else { return false } + return [.proceed, .unspecified].contains(trustResult) + } + + func evaluateAllowing(rootCertificates: [SecCertificate]) -> Bool { + + // Apply our custom root to the trust object. + + var err = SecTrustSetAnchorCertificates(self, rootCertificates as CFArray) + guard err == errSecSuccess else { return false } + + // Re-enable the system's built-in root certificates. + + err = SecTrustSetAnchorCertificatesOnly(self, false) + guard err == errSecSuccess else { return false } + + // Run a trust evaluation and only allow the connection if it succeeds. + + return self.evaluate() + } +} + + +extension UIView{ + func show(){ + self.alpha = 0.0 + self.isHidden = false + UIView.animate(withDuration: 0.25, animations: { + self.alpha = 1 + }) { (complete) in + + } + } + + func hide(){ + UIView.animate(withDuration: 0.25, animations: { + self.alpha = 0.0 + }) { (complete) in + self.isHidden = true + } + } +} + + +extension UIViewController{ + func showAlert(withTitle: String, message: String){ + let alert = UIAlertController(title: withTitle, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .destructive, handler: nil)) + present(alert, animated: true) { + + } + } +} + diff --git a/ios/Helper/FlutterConstants.swift b/ios/Helper/FlutterConstants.swift new file mode 100644 index 0000000..f1b3f09 --- /dev/null +++ b/ios/Helper/FlutterConstants.swift @@ -0,0 +1,36 @@ +// +// FlutterConstants.swift +// Runner +// +// Created by ZiKambrani on 22/12/2020. +// + +import UIKit + +class FlutterConstants{ + static var LOG_GEOFENCE_URL:String? + static var WIFI_CREDENTIALS_URL:String? + static var DEFAULT_HTTP_PARAMS:[String:Any?]? + + class func set(){ + + // (FiX) Take a start with FlutterMethodChannel (kikstart) + /* First call to flutter method is not returning the correct value (Always returning 'NSObject') then after it wroking fine and returning correct value*/ + FlutterText.with(key: "test") { (test) in + + flutterMethodChannel?.invokeMethod("getDefaultHttpParameters", arguments: nil){ (response) in + if let defaultHTTPParams = response as? [String:Any?]{ + DEFAULT_HTTP_PARAMS = defaultHTTPParams + } + + } + + flutterMethodChannel?.invokeMethod("getLogGeofenceFullUrl", arguments:nil){ (response) in + if let url = response as? String{ + LOG_GEOFENCE_URL = url + } + } + + } + } +} diff --git a/ios/Helper/GeoZoneModel.swift b/ios/Helper/GeoZoneModel.swift new file mode 100644 index 0000000..e703b64 --- /dev/null +++ b/ios/Helper/GeoZoneModel.swift @@ -0,0 +1,67 @@ +// +// GeoZoneModel.swift +// Runner +// +// Created by ZiKambrani on 13/12/2020. +// + +import UIKit +import CoreLocation + +class GeoZoneModel{ + var geofenceId:Int = -1 + var description:String = "" + var descriptionN:String? + var latitude:String? + var longitude:String? + var radius:Int? + var type:Int? + var projectID:Int? + var imageURL:String? + var isCity:String? + + func identifier() -> String{ + return "\(geofenceId)_hmg" + } + + func message() -> String{ + return description + } + + func toRegion(locationManager:CLLocationManager) -> CLCircularRegion?{ + if let rad = radius, let lat = latitude?.removeSpace(), let long = longitude?.removeSpace(), + let radius_d = Double("\(rad)"), let lat_d = Double(lat), let long_d = Double(long){ + + let coordinate = CLLocationCoordinate2D(latitude: lat_d, longitude: long_d) + let validatedRadius = min(radius_d, locationManager.maximumRegionMonitoringDistance) + + let region = CLCircularRegion(center: coordinate, radius: validatedRadius, identifier: identifier()) + region.notifyOnExit = true + region.notifyOnEntry = true + return region + } + return nil + } + + class func from(json:[String:Any]) -> GeoZoneModel{ + let model = GeoZoneModel() + model.geofenceId = json["GEOF_ID"] as? Int ?? 0 + model.radius = json["Radius"] as? Int + model.projectID = json["ProjectID"] as? Int + model.type = json["Type"] as? Int + model.description = json["Description"] as? String ?? "" + model.descriptionN = json["DescriptionN"] as? String + model.latitude = json["Latitude"] as? String + model.longitude = json["Longitude"] as? String + model.imageURL = json["ImageURL"] as? String + model.isCity = json["IsCity"] as? String + + return model + } + + class func list(from jsonString:String) -> [GeoZoneModel]{ + let value = dictionaryArray(from: jsonString) + let geoZones = value.map { GeoZoneModel.from(json: $0) } + return geoZones + } +} diff --git a/ios/Helper/GlobalHelper.swift b/ios/Helper/GlobalHelper.swift new file mode 100644 index 0000000..3768780 --- /dev/null +++ b/ios/Helper/GlobalHelper.swift @@ -0,0 +1,119 @@ +// +// GlobalHelper.swift +// Runner +// +// Created by ZiKambrani on 29/03/1442 AH. +// + +import UIKit + +func dictionaryArray(from:String) -> [[String:Any]]{ + if let data = from.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] ?? [] + } catch { + print(error.localizedDescription) + } + } + return [] + +} + +func dictionary(from:String) -> [String:Any]?{ + if let data = from.data(using: .utf8) { + do { + return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + +} + +let HmgLocalNotificationCategoryIdentifier = "hmg.local.notification" +func showNotification(identifier:String? = nil, title:String?, subtitle:String?, message:String?, sound:UNNotificationSound = UNNotificationSound.default, categoryIdentifier:String = HmgLocalNotificationCategoryIdentifier){ + DispatchQueue.main.async { + let notificationContent = UNMutableNotificationContent() + notificationContent.categoryIdentifier = categoryIdentifier + + if identifier != nil { notificationContent.categoryIdentifier = identifier! } + if title != nil { notificationContent.title = title! } + if subtitle != nil { notificationContent.body = message! } + if message != nil { notificationContent.subtitle = subtitle! } + + notificationContent.sound = UNNotificationSound.default + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: "\(Date().timeIntervalSinceNow)", content: notificationContent, trigger: trigger) + + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("Error: \(error)") + } + } + } +} + +func appLanguageCode() -> Int{ + let lang = UserDefaults.standard.string(forKey: "language") ?? "ar" + return lang == "ar" ? 2 : 1 +} + +func userProfile() -> [String:Any?]?{ + var userProf = UserDefaults.standard.string(forKey: "flutter.imei-user-data") + if(userProf == nil){ + userProf = UserDefaults.standard.string(forKey: "flutter.user-profile") + } + return dictionary(from: userProf ?? "{}") +} + +fileprivate let defaultHTTPParams:[String : Any?] = [ + "ZipCode" : "966", + "VersionID" : 5.8, + "Channel" : 3, + "LanguageID" : appLanguageCode(), + "IPAdress" : "10.20.10.20", + "generalid" : "Cs2020@2016$2958", + "PatientOutSA" : 0, + "SessionID" : nil, + "isDentalAllowedBackend" : false, + "DeviceTypeID" : 2 +] + +func httpPostRequest(urlString:String, jsonBody:[String:Any?], completion:((Bool,[String:Any]?)->Void)?){ + var json: [String: Any?] = jsonBody + json = json.merge(dict: defaultHTTPParams) + let jsonData = try? JSONSerialization.data(withJSONObject: json) + + // create post request + let url = URL(string: urlString)! + var request = URLRequest(url: url) + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue("*/*", forHTTPHeaderField: "Accept") + request.httpMethod = "POST" + request.httpBody = jsonData + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, error == nil else { + print(error?.localizedDescription ?? "No data") + return + } + + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + if let responseJSON = responseJSON as? [String: Any], let status = responseJSON["MessageStatus"] as? Int{ + print(responseJSON) + if status == 1{ + completion?(true,responseJSON) + }else{ + completion?(false,responseJSON) + } + + }else{ + completion?(false,nil) + } + } + + task.resume() + +} diff --git a/ios/Helper/HMGPenguinInPlatformBridge.swift b/ios/Helper/HMGPenguinInPlatformBridge.swift new file mode 100644 index 0000000..c4a4424 --- /dev/null +++ b/ios/Helper/HMGPenguinInPlatformBridge.swift @@ -0,0 +1,94 @@ +import Foundation +import FLAnimatedImage + + +var flutterMethodChannelPenguinIn:FlutterMethodChannel? = nil +fileprivate var mainViewController:MainFlutterVC! + +class HMGPenguinInPlatformBridge{ + + private let channelName = "launch_penguin_ui" + private static var shared_:HMGPenguinInPlatformBridge? + + class func initialize(flutterViewController:MainFlutterVC){ + shared_ = HMGPenguinInPlatformBridge() + mainViewController = flutterViewController + shared_?.openChannel() + } + + func shared() -> HMGPenguinInPlatformBridge{ + assert((HMGPenguinInPlatformBridge.shared_ != nil), "HMGPenguinInPlatformBridge is not initialized, call initialize(mainViewController:MainFlutterVC) function first.") + return HMGPenguinInPlatformBridge.shared_! + } + + private func openChannel(){ + flutterMethodChannelPenguinIn = FlutterMethodChannel(name: channelName, binaryMessenger: mainViewController.binaryMessenger) + + flutterMethodChannelPenguinIn?.setMethodCallHandler { (methodCall, result) in + print("Called function \(methodCall.method)") + + if let arguments = methodCall.arguments as Any? { + if methodCall.method == "launchPenguin"{ + print("====== launchPenguinView Launched =========") + self.launchPenguinView(arguments: arguments, result: result) + } + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Storyboard name is required", details: nil)) + } + } + } + + private func launchPenguinView(arguments: Any, result: @escaping FlutterResult) { + + let penguinView = PenguinView( + frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height), + viewIdentifier: 0, + arguments: arguments, + binaryMessenger: mainViewController.binaryMessenger + ) + + let penguinUIView = penguinView.view() + penguinUIView.frame = mainViewController.view.bounds + penguinUIView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + mainViewController.view.addSubview(penguinUIView) + + guard let args = arguments as? [String: Any], + let loaderImageData = args["loaderImage"] as? FlutterStandardTypedData else { + print("loaderImage data not found in arguments") + result(FlutterError(code: "ARGUMENT_ERROR", message: "Missing loaderImage data", details: nil)) + return + } + + let loadingOverlay = UIView(frame: UIScreen.main.bounds) + loadingOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.5) // Semi-transparent overlay + loadingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + // Display the GIF using FLAnimatedImage + let animatedImage = FLAnimatedImage(animatedGIFData: loaderImageData.data) + let gifImageView = FLAnimatedImageView() + gifImageView.animatedImage = animatedImage + gifImageView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) + gifImageView.center = loadingOverlay.center + gifImageView.contentMode = .scaleAspectFit + loadingOverlay.addSubview(gifImageView) + + + if let window = UIApplication.shared.windows.first { + window.addSubview(loadingOverlay) + + } else { + print("Error: Main window not found") + } + + penguinView.onSuccess = { + // Hide and remove the loader + DispatchQueue.main.async { + loadingOverlay.removeFromSuperview() + + } + } + + result(nil) + } +} diff --git a/ios/Helper/HMGPlatformBridge.swift b/ios/Helper/HMGPlatformBridge.swift new file mode 100644 index 0000000..fd9fb40 --- /dev/null +++ b/ios/Helper/HMGPlatformBridge.swift @@ -0,0 +1,140 @@ +// +// HMGPlatformBridge.swift +// Runner +// +// Created by ZiKambrani on 14/12/2020. +// + +import UIKit +import NetworkExtension +import SystemConfiguration.CaptiveNetwork + +var flutterMethodChannel:FlutterMethodChannel? = nil +fileprivate var mainViewController:MainFlutterVC! + +class HMGPlatformBridge{ + private let channelName = "HMG-Platform-Bridge" + private static var shared_:HMGPlatformBridge? + + class func initialize(flutterViewController:MainFlutterVC){ + shared_ = HMGPlatformBridge() + mainViewController = flutterViewController + shared_?.openChannel() + } + + func shared() -> HMGPlatformBridge{ + assert((HMGPlatformBridge.shared_ != nil), "HMGPlatformBridge is not initialized, call initialize(mainViewController:MainFlutterVC) function first.") + return HMGPlatformBridge.shared_! + } + + private func openChannel(){ + flutterMethodChannel = FlutterMethodChannel(name: channelName, binaryMessenger: mainViewController.binaryMessenger) + flutterMethodChannel?.setMethodCallHandler { (methodCall, result) in + print("Called function \(methodCall.method)") + if methodCall.method == "connectHMGInternetWifi"{ + self.connectHMGInternetWifi(methodCall:methodCall, result: result) + + }else if methodCall.method == "connectHMGGuestWifi"{ + self.connectHMGGuestWifi(methodCall:methodCall, result: result) + + }else if methodCall.method == "isHMGNetworkAvailable"{ + self.isHMGNetworkAvailable(methodCall:methodCall, result: result) + + }else if methodCall.method == "registerHmgGeofences"{ + self.registerHmgGeofences(result: result) + + }else if methodCall.method == "unRegisterHmgGeofences"{ + self.unRegisterHmgGeofences(result: result) + } + + print("") + } + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { (timer) in + FlutterConstants.set() + } + } + + + + // Connect HMG Wifi and Internet + func connectHMGInternetWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + + guard let pateintId = (methodCall.arguments as? [Any])?.first as? String + else { return assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") } + + + HMG_Internet.shared.connect(patientId: pateintId) { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + // Connect HMG-Guest for App Access + func connectHMGGuestWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + HMG_GUEST.shared.connect() { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + func isHMGNetworkAvailable(methodCall:FlutterMethodCall ,result: @escaping FlutterResult) -> Bool{ + guard let ssid = methodCall.arguments as? String else { + assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") + return false + } + + let queue = DispatchQueue.init(label: "com.hmg.wifilist") + NEHotspotHelper.register(options: nil, queue: queue) { (command) in + print(command) + + if(command.commandType == NEHotspotHelperCommandType.filterScanList) { + if let networkList = command.networkList{ + for network in networkList{ + print(network.ssid) + } + } + } + } + return false + + } + + + // Message Dailog + func showMessage(title:String, message:String){ + DispatchQueue.main.async { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert ) + alert.addAction(UIAlertAction(title: "OK", style: .destructive, handler: nil)) + mainViewController.present(alert, animated: true) { + + } + } + } + + // Register Geofence + func registerHmgGeofences(result: @escaping FlutterResult){ + flutterMethodChannel?.invokeMethod("getGeoZones", arguments: nil){ geoFencesJsonString in + if let jsonString = geoFencesJsonString as? String{ + let allZones = GeoZoneModel.list(from: jsonString) + HMG_Geofence.shared().register(geoZones: allZones) + result(true) + }else{ + } + } + } + + // Register Geofence + func unRegisterHmgGeofences(result: @escaping FlutterResult){ + HMG_Geofence.shared().unRegisterAll() + result(true) + } + +} diff --git a/ios/Helper/HMG_Geofence.swift b/ios/Helper/HMG_Geofence.swift new file mode 100644 index 0000000..47454d3 --- /dev/null +++ b/ios/Helper/HMG_Geofence.swift @@ -0,0 +1,183 @@ +// +// HMG_Geofence.swift +// Runner +// +// Created by ZiKambrani on 13/12/2020. +// + +import UIKit +import CoreLocation + +fileprivate var df = DateFormatter() +fileprivate var transition = "" + +enum Transition:Int { + case entry = 1 + case exit = 2 + func name() -> String{ + return self.rawValue == 1 ? "Enter" : "Exit" + } +} + +class HMG_Geofence:NSObject{ + + var geoZones:[GeoZoneModel]? + var locationManager:CLLocationManager!{ + didSet{ + // https://developer.apple.com/documentation/corelocation/cllocationmanager/1423531-startmonitoringsignificantlocati + + locationManager.allowsBackgroundLocationUpdates = true + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.activityType = .other + locationManager.delegate = self + locationManager.requestAlwaysAuthorization() + // locationManager.distanceFilter = 500 + // locationManager.startMonitoringSignificantLocationChanges() + } + } + + private static var shared_:HMG_Geofence? + class func shared() -> HMG_Geofence{ + if HMG_Geofence.shared_ == nil{ + HMG_Geofence.initGeofencing() + } + return shared_! + } + + class func initGeofencing(){ + shared_ = HMG_Geofence() + shared_?.locationManager = CLLocationManager() + } + + func register(geoZones:[GeoZoneModel]){ + + self.geoZones = geoZones + + let monitoredRegions_ = monitoredRegions() + self.geoZones?.forEach({ (zone) in + if let region = zone.toRegion(locationManager: locationManager){ + if let already = monitoredRegions_.first(where: {$0.identifier == zone.identifier()}){ + debugPrint("Already monitering region: \(already)") + }else{ + startMonitoring(region: region) + } + }else{ + debugPrint("Invalid region: \(zone.latitude ?? "invalid_latitude"),\(zone.longitude ?? "invalid_longitude"),r\(zone.radius ?? 0) | \(zone.identifier())") + } + }) + } + + func monitoredRegions() -> Set{ + return locationManager.monitoredRegions + } + + func unRegisterAll(){ + for region in locationManager.monitoredRegions { + locationManager.stopMonitoring(for: region) + } + } + +} + +// CLLocationManager Delegates +extension HMG_Geofence : CLLocationManagerDelegate{ + + func startMonitoring(region: CLCircularRegion) { + if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) { + return + } + + if CLLocationManager.authorizationStatus() != .authorizedAlways { + let message = """ + Your geotification is saved but will only be activated once you grant + HMG permission to access the device location. + """ + debugPrint(message) + } + + locationManager.startMonitoring(for: region) + locationManager.requestState(for: region) + debugPrint("Starts monitering region: \(region)") + } + + func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { + debugPrint("didEnterRegion: \(region)") + if region is CLCircularRegion { + handleEvent(for: region,transition: .entry, location: manager.location) + } + } + + func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { + debugPrint("didExitRegion: \(region)") + if region is CLCircularRegion { + handleEvent(for: region,transition: .exit, location: manager.location) + } + } + + func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) { + debugPrint("didDetermineState: \(state)") + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + debugPrint("didUpdateLocations: \(locations)") + } + + +} + +// Helpers +extension HMG_Geofence{ + + func handleEvent(for region: CLRegion!, transition:Transition, location:CLLocation?) { + if let userProfile = userProfile(){ + notifyUser(forRegion: region, transition: transition, location: locationManager.location, userProfile: userProfile) + notifyServer(forRegion: region, transition: transition, location: locationManager.location, userProfile: userProfile) + } + } + + func geoZone(by id: String) -> GeoZoneModel? { + var zone:GeoZoneModel? = nil + if let zones_ = geoZones{ + zone = zones_.first(where: { $0.identifier() == id}) + }else{ + // let jsonArray = UserDefaults.standard.string(forKey: "hmg-geo-fences") + } + return zone + } + + + func notifyUser(forRegion:CLRegion, transition:Transition, location:CLLocation?, userProfile:[String:Any?]){ + if let patientId = userProfile["PatientID"] as? Int{ + + } + } + + func notifyServer(forRegion:CLRegion, transition:Transition, location:CLLocation?, userProfile:[String:Any?]){ + if let patientId = userProfile["PatientID"] as? Int{ + + if let idString = forRegion.identifier.split(separator: "_").first, let idInt = Int(idString){ + let body:[String:Any] = [ + "PointsID":idInt, + "GeoType":transition.rawValue, + "PatientID":patientId + ] + + var logs = UserDefaults.init(suiteName: "GeoFenceLog")?.dictionary(forKey: "GEOFENCE_LOGS") ?? [:] + var geo = (logs[forRegion.identifier] as? [String]) ?? [] + + let url = "https://hmgwebservices.com/Services/Patients.svc/REST/GeoF_InsertPatientFileInfo" + httpPostRequest(urlString: url, jsonBody: body){ (status,json) in + let status_ = status ? "Notified successfully:" : "Failed to notify:" + showNotification(title: transition.name(), subtitle: forRegion.identifier, message: status_) + + + geo.append("\(status_) \(transition.name()) at \(Date().toString(format: "dd/MMM/yyy hh:mm:ss"))") + logs.updateValue( geo, forKey: forRegion.identifier) + + UserDefaults.init(suiteName: "GeoFenceLog")?.set(logs, forKey: "GEOFENCE_LOGS") + } + } + } + } +} + diff --git a/ios/Helper/LocalizedFromFlutter.swift b/ios/Helper/LocalizedFromFlutter.swift new file mode 100644 index 0000000..8853064 --- /dev/null +++ b/ios/Helper/LocalizedFromFlutter.swift @@ -0,0 +1,22 @@ +// +// LocalizedFromFlutter.swift +// Runner +// +// Created by ZiKambrani on 10/04/1442 AH. +// + +import UIKit + +class FlutterText{ + + class func with(key:String,completion: @escaping (String)->Void){ + flutterMethodChannel?.invokeMethod("localizedValue", arguments: key, result: { (result) in + if let localized = result as? String{ + completion(localized) + }else{ + completion(key) + } + }) + } + +} diff --git a/ios/Helper/OpenTokPlatformBridge.swift b/ios/Helper/OpenTokPlatformBridge.swift new file mode 100644 index 0000000..4da39dc --- /dev/null +++ b/ios/Helper/OpenTokPlatformBridge.swift @@ -0,0 +1,61 @@ +// +// HMGPlatformBridge.swift +// Runner +// +// Created by ZiKambrani on 14/12/2020. +// + +import UIKit +import NetworkExtension +import SystemConfiguration.CaptiveNetwork + + +fileprivate var openTok:OpenTok? + +class OpenTokPlatformBridge : NSObject{ + private var methodChannel:FlutterMethodChannel? = nil + private var mainViewController:MainFlutterVC! + private static var shared_:OpenTokPlatformBridge? + + class func initialize(flutterViewController:MainFlutterVC, registrar:FlutterPluginRegistrar?){ + shared_ = OpenTokPlatformBridge() + shared_?.mainViewController = flutterViewController + + shared_?.openChannel() + openTok = OpenTok(mainViewController: flutterViewController, registrar: registrar) + } + + func shared() -> OpenTokPlatformBridge{ + assert((OpenTokPlatformBridge.shared_ != nil), "OpenTokPlatformBridge is not initialized, call initialize(mainViewController:MainFlutterVC) function first.") + return OpenTokPlatformBridge.shared_! + } + + private func openChannel(){ + methodChannel = FlutterMethodChannel(name: "OpenTok-Platform-Bridge", binaryMessenger: mainViewController.binaryMessenger) + methodChannel?.setMethodCallHandler { (call, result) in + print("Called function \(call.method)") + + switch(call.method) { + case "initSession": + openTok?.initSession(call: call, result: result) + + case "swapCamera": + openTok?.swapCamera(call: call, result: result) + + case "toggleAudio": + openTok?.toggleAudio(call: call, result: result) + + case "toggleVideo": + openTok?.toggleVideo(call: call, result: result) + + case "hangupCall": + openTok?.hangupCall(call: call, result: result) + + default: + result(FlutterMethodNotImplemented) + } + + print("") + } + } +} diff --git a/ios/Penguin/PenguinModel.swift b/ios/Penguin/PenguinModel.swift new file mode 100644 index 0000000..e41979d --- /dev/null +++ b/ios/Penguin/PenguinModel.swift @@ -0,0 +1,76 @@ +// +// PenguinModel.swift +// Runner +// +// Created by Amir on 06/08/2024. +// + +import Foundation + +// Define the model class +struct PenguinModel { + let baseURL: String + let dataURL: String + let dataServiceName: String + let positionURL: String + let clientKey: String + let storyboardName: String + let mapBoxKey: String + let clientID: String + let positionServiceName: String + let username: String + let isSimulationModeEnabled: Bool + let isShowUserName: Bool + let isUpdateUserLocationSmoothly: Bool + let isEnableReportIssue: Bool + let languageCode: String + let clinicID: String + let patientID: String + let projectID: String + + // Initialize the model from a dictionary + init?(from dictionary: [String: Any]) { + guard + let baseURL = dictionary["baseURL"] as? String, + let dataURL = dictionary["dataURL"] as? String, + let dataServiceName = dictionary["dataServiceName"] as? String, + let positionURL = dictionary["positionURL"] as? String, + let clientKey = dictionary["clientKey"] as? String, + let storyboardName = dictionary["storyboardName"] as? String, + let mapBoxKey = dictionary["mapBoxKey"] as? String, + let clientID = dictionary["clientID"] as? String, + let positionServiceName = dictionary["positionServiceName"] as? String, + let username = dictionary["username"] as? String, + let isSimulationModeEnabled = dictionary["isSimulationModeEnabled"] as? Bool, + let isShowUserName = dictionary["isShowUserName"] as? Bool, + let isUpdateUserLocationSmoothly = dictionary["isUpdateUserLocationSmoothly"] as? Bool, + let isEnableReportIssue = dictionary["isEnableReportIssue"] as? Bool, + let languageCode = dictionary["languageCode"] as? String, + let clinicID = dictionary["clinicID"] as? String, + let patientID = dictionary["patientID"] as? String, + let projectID = dictionary["projectID"] as? String + else { + print("Initialization failed due to missing or invalid keys.") + return nil + } + + self.baseURL = baseURL + self.dataURL = dataURL + self.dataServiceName = dataServiceName + self.positionURL = positionURL + self.clientKey = clientKey + self.storyboardName = storyboardName + self.mapBoxKey = mapBoxKey + self.clientID = clientID + self.positionServiceName = positionServiceName + self.username = username + self.isSimulationModeEnabled = isSimulationModeEnabled + self.isShowUserName = isShowUserName + self.isUpdateUserLocationSmoothly = isUpdateUserLocationSmoothly + self.isEnableReportIssue = isEnableReportIssue + self.languageCode = languageCode + self.clinicID = clinicID + self.patientID = patientID + self.projectID = projectID + } +} diff --git a/ios/Penguin/PenguinNavigator.swift b/ios/Penguin/PenguinNavigator.swift new file mode 100644 index 0000000..e7ce55b --- /dev/null +++ b/ios/Penguin/PenguinNavigator.swift @@ -0,0 +1,57 @@ +import PenNavUI +import UIKit + +class PenguinNavigator { + private var config: PenguinModel + + init(config: PenguinModel) { + self.config = config + } + + private func logError(_ message: String) { + // Centralized logging function + print("PenguinSDKNavigator Error: \(message)") + } + + func navigateToPOI( referenceId:String,completion: @escaping (Bool, String?) -> Void) { + PenNavUIManager.shared.getToken(clientID: config.clientID, clientKey: config.clientKey) { [weak self] token, error in + + if let error = error { + let errorMessage = "Token error while getting the for Navigate to method" + completion(false, "Failed to get token: \(errorMessage)") + + print("Failed to get token: \(errorMessage)") + return + } + + guard let token = token else { + completion(false, "Token is nil") + print("Token is nil") + return + } + print("Token Generated") + print(token); + + + } + } + + private func handleNavigation(referenceId: String, token: String, completion: @escaping (Bool, String?) -> Void) { + DispatchQueue.main.async { + PenNavUIManager.shared.setToken(token: token) + + PenNavUIManager.shared.navigate(to: referenceId) { [weak self] _, navError in + guard let self = self else { return } + + if let navError = navError { + self.logError("Navigation error: Reference ID invalid") + completion(false, "Navigation error: \(navError.localizedDescription)") + return + } + + // Navigation successful + completion(true, nil) + } + } + } +} diff --git a/ios/Penguin/PenguinPlugin.swift b/ios/Penguin/PenguinPlugin.swift new file mode 100644 index 0000000..029bec3 --- /dev/null +++ b/ios/Penguin/PenguinPlugin.swift @@ -0,0 +1,31 @@ +// +// BlueGpsPlugin.swift +// Runner +// +// Created by Penguin . +// + +//import Foundation +//import Flutter +// +///** +// * A Flutter plugin for integrating Penguin SDK functionality. +// * This class registers a view factory with the Flutter engine to create native views. +// */ +//class PenguinPlugin: NSObject, FlutterPlugin { +// +// /** +// * Registers the plugin with the Flutter engine. +// * +// * @param registrar The [FlutterPluginRegistrar] used to register the plugin. +// * This method is called when the plugin is initialized, and it sets up the communication +// * between Flutter and native code. +// */ +// public static func register(with registrar: FlutterPluginRegistrar) { +// // Create an instance of PenguinViewFactory with the binary messenger from the registrar +// let factory = PenguinViewFactory(messenger: registrar.messenger()) +// +// // Register the view factory with a unique ID for use in Flutter code +// registrar.register(factory, withId: "penguin_native") +// } +//} diff --git a/ios/Penguin/PenguinView.swift b/ios/Penguin/PenguinView.swift new file mode 100644 index 0000000..b5161eb --- /dev/null +++ b/ios/Penguin/PenguinView.swift @@ -0,0 +1,445 @@ +// + +// BlueGpsView.swift + +// Runner + +// + +// Created by Penguin. + +// + + + +import Foundation +import UIKit +import Flutter +import PenNavUI + +import Foundation +import Flutter +import UIKit + + + +/** + + * A custom Flutter platform view for displaying Penguin UI components. + + * This class integrates with the Penguin navigation SDK and handles UI events. + + */ + +class PenguinView: NSObject, FlutterPlatformView, PIEventsDelegate, PenNavInitializationDelegate + +{ + // The main view displayed within the platform view + + private var _view: UIView + + private var model: PenguinModel? + + private var methodChannel: FlutterMethodChannel + + var onSuccess: (() -> Void)? + + + + + + + + /** + + * Initializes the PenguinView with the provided parameters. + + * + + * @param frame The frame of the view, specifying its size and position. + + * @param viewId A unique identifier for this view instance. + + * @param args Optional arguments provided for creating the view. + + * @param messenger The [FlutterBinaryMessenger] used for communication with Dart. + + */ + + init( + + frame: CGRect, + + viewIdentifier viewId: Int64, + + arguments args: Any?, + + binaryMessenger messenger: FlutterBinaryMessenger? + + ) { + + _view = UIView() + + methodChannel = FlutterMethodChannel(name: "launch_penguin_ui", binaryMessenger: messenger!) + + + + super.init() + + + + // Get the screen's width and height to set the view's frame + + let screenWidth = UIScreen.main.bounds.width + + let screenHeight = UIScreen.main.bounds.height + + + + // Uncomment to set the background color of the view + + // _view.backgroundColor = UIColor.red + + + + // Set the frame of the view to cover the entire screen + + _view.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight) + + print("========Inside Penguin View ========") + + print(args) + + guard let arguments = args as? [String: Any] else { + + print("Error: Arguments are not in the expected format.") + + return + + } + + print("===== i got tha Args=======") + + + + // Initialize the model from the arguments + + if let penguinModel = PenguinModel(from: arguments) { + + self.model = penguinModel + + initPenguin(args: penguinModel) + + } else { + + print("Error: Failed to initialize PenguinModel from arguments ") + + } + + // Initialize the Penguin SDK with required configurations + + // initPenguin( arguments: args) + + } + + + + /** + + * Initializes the Penguin SDK with custom configuration settings. + + */ + + func initPenguin(args: PenguinModel) { + +// Set the initialization delegate to handle SDK initialization events + + PenNavUIManager.shared.initializationDelegate = self + + // Configure the Penguin SDK with necessary parameters + + PenNavUIManager.shared + + .setClientKey(args.clientKey) + + .setClientID(args.clientID) + + .setUsername(args.username) + + .setSimulationModeEnabled(isEnable: args.isSimulationModeEnabled) + + .setBaseURL(dataURL: args.dataURL, positionURL: args.positionURL) + + .setServiceName(dataServiceName: args.dataServiceName, positionServiceName: args.positionServiceName) + + .setIsShowUserName(args.isShowUserName) + + .setIsUpdateUserLocationSmoothly(args.isUpdateUserLocationSmoothly) + + .setEnableReportIssue(enable: args.isEnableReportIssue) + + .setLanguage(args.languageCode) + + .setBackButtonVisibility(true) + + .build() + + } + + + + + + /** + + * Returns the main view associated with this platform view. + + * + + * @return The UIView instance that represents this platform view. + + */ + + func view() -> UIView { + + return _view + + } + + + + // MARK: - PIEventsDelegate Methods + + + + + + + + + + /** + + * Called when the Penguin UI is dismissed. + + */ + + func onPenNavUIDismiss() { + + // Handle UI dismissal if needed + + print("====== onPenNavUIDismiss =========") + + + + + + self.view().removeFromSuperview() + + } + + + + /** + + * Called when a report issue is generated. + + * + + * @param issue The type of issue reported. + + */ + + func onReportIssue(_ issue: PenNavUI.IssueType) { + + // Handle report issue events if needed + + print("====== onReportIssueError =========") + + methodChannel.invokeMethod("onReportIssue", arguments: ["issueType": issue]) + + + + } + + + + /** + + * Called when the Penguin UI setup is successful. + + */ + + func onPenNavSuccess() { + + print("====== onPenNavSuccess =========") + + onSuccess?() + + methodChannel.invokeMethod("onPenNavSuccess", arguments: nil) + + // Obtain the FlutterViewController instance + + let controller: FlutterViewController = UIApplication.shared.windows.first?.rootViewController as! FlutterViewController + + + + print("====== after controller onPenNavSuccess =========") + + + + // Set the events delegate to handle SDK events + + PenNavUIManager.shared.eventsDelegate = self + + + + print("====== after eventsDelegate onPenNavSuccess =========") + + + + // Present the Penguin UI on top of the Flutter view controller + + PenNavUIManager.shared.present(root: controller, view: _view) + + + + + + print("====== after present onPenNavSuccess =========") + + print(model?.clinicID) + + print("====== after present onPenNavSuccess =========") + + + + guard let config = self.model else { + + print("Error: Config Model is nil") + + return + + } + + + + guard let clinicID = self.model?.clinicID, + + let clientID = self.model?.clientID, !clientID.isEmpty else { + + print("Error: Config Client ID is nil or empty") + + return + + } + + + + let navigator = PenguinNavigator(config: config) + + + + PenNavUIManager.shared.getToken(clientID: config.clientID, clientKey: config.clientKey) { [weak self] token, error in + + if let error = error { + + let errorMessage = "Token error while getting the for Navigate to method" + + print("Failed to get token: \(errorMessage)") + + return + + } + + + + guard let token = token else { + + print("Token is nil") + + return + + } + + print("Token Generated") + + print(token); + + + + self?.handleNavigation(clinicID: clinicID, token: token) { success, errorMessage in + + if success { + + print("Navigation successful") + + } else { + + print("Navigation failed: \(errorMessage ?? "Unknown error")") + + } + + + + } + + + + print("====== after Token onPenNavSuccess =========") + + } + + + + } + + + + + + + + private func handleNavigation(clinicID: String, token: String, completion: @escaping (Bool, String?) -> Void) { + + DispatchQueue.main.async { + + PenNavUIManager.shared.setToken(token: token) + + PenNavUIManager.shared.navigate(to: clinicID) + + completion(true,nil) + + } + + } + + + + + + + + + + /** + + * Called when there is an initialization error with the Penguin UI. + + * + + * @param errorType The type of initialization error. + + * @param errorDescription A description of the error. + + */ + + func onPenNavInitializationError(errorType: PenNavUI.PenNavUIError, errorDescription: String) { + + // Handle initialization errors if needed + + print("onPenNavInitializationErrorType: \(errorType.rawValue)") + + print("onPenNavInitializationError: \(errorDescription)") + } +} diff --git a/ios/Penguin/PenguinViewFactory.swift b/ios/Penguin/PenguinViewFactory.swift new file mode 100644 index 0000000..a88bb5d --- /dev/null +++ b/ios/Penguin/PenguinViewFactory.swift @@ -0,0 +1,59 @@ +// +// BlueGpsViewFactory.swift +// Runner +// +// Created by Penguin . +// + +import Foundation +import Flutter + +/** + * A factory class for creating instances of [PenguinView]. + * This class implements `FlutterPlatformViewFactory` to create and manage native views. + */ +class PenguinViewFactory: NSObject, FlutterPlatformViewFactory { + + // The binary messenger used for communication with the Flutter engine + private var messenger: FlutterBinaryMessenger + + /** + * Initializes the PenguinViewFactory with the given messenger. + * + * @param messenger The [FlutterBinaryMessenger] used to communicate with Dart code. + */ + init(messenger: FlutterBinaryMessenger) { + self.messenger = messenger + super.init() + } + + /** + * Creates a new instance of [PenguinView]. + * + * @param frame The frame of the view, specifying its size and position. + * @param viewId A unique identifier for this view instance. + * @param args Optional arguments provided for creating the view. + * @return An instance of [PenguinView] configured with the provided parameters. + */ + func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return PenguinView( + frame: frame, + viewIdentifier: viewId, + arguments: args, + binaryMessenger: messenger) + } + + /** + * Returns the codec used for encoding and decoding method channel arguments. + * This method is required when `arguments` in `create` is not `nil`. + * + * @return A [FlutterMessageCodec] instance used for serialization. + */ + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } +} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2eab03a..7a41ae2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,11 +11,23 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 478CFA942E638C8E0064F3D7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */; }; + 61243B492EC5FA3700D46FA0 /* PenguinModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B422EC5FA3700D46FA0 /* PenguinModel.swift */; }; + 61243B4C2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B3D2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift */; }; + 61243B502EC5FA3700D46FA0 /* PenguinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B452EC5FA3700D46FA0 /* PenguinView.swift */; }; + 61243B542EC5FA3700D46FA0 /* PenguinPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B442EC5FA3700D46FA0 /* PenguinPlugin.swift */; }; + 61243B562EC5FA3700D46FA0 /* PenguinNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B432EC5FA3700D46FA0 /* PenguinNavigator.swift */; }; + 61243B572EC5FA3700D46FA0 /* PenguinViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B462EC5FA3700D46FA0 /* PenguinViewFactory.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 766D8CB62EC60BE600D05E07 /* Penguin.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 766D8CB32EC60BE600D05E07 /* Penguin.xcframework */; }; + 766D8CB72EC60BE600D05E07 /* Penguin.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 766D8CB32EC60BE600D05E07 /* Penguin.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 766D8CB82EC60BE600D05E07 /* PenNavUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 766D8CB42EC60BE600D05E07 /* PenNavUI.xcframework */; }; + 766D8CB92EC60BE600D05E07 /* PenNavUI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 766D8CB42EC60BE600D05E07 /* PenNavUI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 766D8CBA2EC60BE600D05E07 /* PenguinINRenderer.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 766D8CB52EC60BE600D05E07 /* PenguinINRenderer.xcframework */; }; + 766D8CBB2EC60BE600D05E07 /* PenguinINRenderer.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 766D8CB52EC60BE600D05E07 /* PenguinINRenderer.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B976FB9C47411C32B24D5E01 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACE60DF9393168FD748550B3 /* Pods_Runner.framework */; }; + DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,6 +47,9 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 766D8CB72EC60BE600D05E07 /* Penguin.xcframework in Embed Frameworks */, + 766D8CBB2EC60BE600D05E07 /* PenguinINRenderer.xcframework in Embed Frameworks */, + 766D8CB92EC60BE600D05E07 /* PenNavUI.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -49,9 +64,18 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 478CFA952E6E20A60064F3D7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 61243B3D2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HMGPenguinInPlatformBridge.swift; sourceTree = ""; }; + 61243B422EC5FA3700D46FA0 /* PenguinModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenguinModel.swift; sourceTree = ""; }; + 61243B432EC5FA3700D46FA0 /* PenguinNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenguinNavigator.swift; sourceTree = ""; }; + 61243B442EC5FA3700D46FA0 /* PenguinPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenguinPlugin.swift; sourceTree = ""; }; + 61243B452EC5FA3700D46FA0 /* PenguinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenguinView.swift; sourceTree = ""; }; + 61243B462EC5FA3700D46FA0 /* PenguinViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenguinViewFactory.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 766D8CB32EC60BE600D05E07 /* Penguin.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Penguin.xcframework; path = Frameworks/Penguin.xcframework; sourceTree = ""; }; + 766D8CB42EC60BE600D05E07 /* PenNavUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PenNavUI.xcframework; path = Frameworks/PenNavUI.xcframework; sourceTree = ""; }; + 766D8CB52EC60BE600D05E07 /* PenguinINRenderer.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PenguinINRenderer.xcframework; path = Frameworks/PenguinINRenderer.xcframework; sourceTree = ""; }; 769C9BF82E6F106D009F68A9 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; @@ -62,7 +86,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - ACE60DF9393168FD748550B3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -71,7 +95,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B976FB9C47411C32B24D5E01 /* Pods_Runner.framework in Frameworks */, + 766D8CB62EC60BE600D05E07 /* Penguin.xcframework in Frameworks */, + 766D8CBA2EC60BE600D05E07 /* PenguinINRenderer.xcframework in Frameworks */, + 766D8CB82EC60BE600D05E07 /* PenNavUI.xcframework in Frameworks */, + DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -86,6 +113,37 @@ path = RunnerTests; sourceTree = ""; }; + 61243B412EC5FA3700D46FA0 /* Helper */ = { + isa = PBXGroup; + children = ( + 61243B3D2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 61243B472EC5FA3700D46FA0 /* Penguin */ = { + isa = PBXGroup; + children = ( + 61243B422EC5FA3700D46FA0 /* PenguinModel.swift */, + 61243B432EC5FA3700D46FA0 /* PenguinNavigator.swift */, + 61243B442EC5FA3700D46FA0 /* PenguinPlugin.swift */, + 61243B452EC5FA3700D46FA0 /* PenguinView.swift */, + 61243B462EC5FA3700D46FA0 /* PenguinViewFactory.swift */, + ); + path = Penguin; + sourceTree = ""; + }; + 766D8CB22EC60BE600D05E07 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 766D8CB32EC60BE600D05E07 /* Penguin.xcframework */, + 766D8CB52EC60BE600D05E07 /* PenguinINRenderer.xcframework */, + 766D8CB42EC60BE600D05E07 /* PenNavUI.xcframework */, + D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 79DD2093A1D9674C94359FC8 /* Pods */ = { isa = PBXGroup; children = ( @@ -115,7 +173,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 79DD2093A1D9674C94359FC8 /* Pods */, - A07D637C76A0ABB38659D189 /* Frameworks */, + 766D8CB22EC60BE600D05E07 /* Frameworks */, ); sourceTree = ""; }; @@ -131,6 +189,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 61243B412EC5FA3700D46FA0 /* Helper */, + 61243B472EC5FA3700D46FA0 /* Penguin */, 769C9BF82E6F106D009F68A9 /* RunnerDebug.entitlements */, 478CFA952E6E20A60064F3D7 /* Runner.entitlements */, 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */, @@ -146,14 +206,6 @@ path = Runner; sourceTree = ""; }; - A07D637C76A0ABB38659D189 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ACE60DF9393168FD748550B3 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -362,6 +414,12 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 61243B492EC5FA3700D46FA0 /* PenguinModel.swift in Sources */, + 61243B4C2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift in Sources */, + 61243B502EC5FA3700D46FA0 /* PenguinView.swift in Sources */, + 61243B542EC5FA3700D46FA0 /* PenguinPlugin.swift in Sources */, + 61243B562EC5FA3700D46FA0 /* PenguinNavigator.swift in Sources */, + 61243B572EC5FA3700D46FA0 /* PenguinViewFactory.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6a5d34f..64d7428 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Flutter import UIKit -//import FirebaseCore -//import FirebaseMessaging +import FirebaseCore +import FirebaseMessaging import GoogleMaps @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,11 +10,18 @@ import GoogleMaps didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GMSServices.provideAPIKey("AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng") -// FirebaseApp.configure() + FirebaseApp.configure() + initializePlatformChannels() GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - + func initializePlatformChannels(){ + if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground + + HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController) + + } + } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken:Data){ // Messaging.messaging().apnsToken = deviceToken super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) diff --git a/ios/Runner/Controllers/MainFlutterVC.swift b/ios/Runner/Controllers/MainFlutterVC.swift new file mode 100644 index 0000000..4f91d05 --- /dev/null +++ b/ios/Runner/Controllers/MainFlutterVC.swift @@ -0,0 +1,118 @@ +// +// MainFlutterVC.swift +// Runner +// +// Created by ZiKambrani on 25/03/1442 AH. +// + +import UIKit +import Flutter +import NetworkExtension +import SystemConfiguration.CaptiveNetwork + +class MainFlutterVC: FlutterViewController { + + override func viewDidLoad() { + super.viewDidLoad() + +// flutterMethodChannel?.setMethodCallHandler { (methodCall, result) in +// +// if methodCall.method == "connectHMGInternetWifi"{ +// self.connectHMGInternetWifi(methodCall:methodCall, result: result) +// +// }else if methodCall.method == "connectHMGGuestWifi"{ +// self.connectHMGGuestWifi(methodCall:methodCall, result: result) +// +// }else if methodCall.method == "isHMGNetworkAvailable"{ +// self.isHMGNetworkAvailable(methodCall:methodCall, result: result) +// +// }else if methodCall.method == "registerHmgGeofences"{ +// self.registerHmgGeofences(result: result) +// } +// +// print("") +// } +// +// FlutterText.with(key: "errorConnectingHmgNetwork") { (localized) in +// print(localized) +// } + + } + + + // Connect HMG Wifi and Internet + func connectHMGInternetWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + + guard let pateintId = (methodCall.arguments as? [Any])?.first as? String + else { return assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") } + + + HMG_Internet.shared.connect(patientId: pateintId) { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + // Connect HMG-Guest for App Access + func connectHMGGuestWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + HMG_GUEST.shared.connect() { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + func isHMGNetworkAvailable(methodCall:FlutterMethodCall ,result: @escaping FlutterResult) -> Bool{ + guard let ssid = methodCall.arguments as? String else { + assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") + return false + } + + let queue = DispatchQueue.init(label: "com.hmg.wifilist") + NEHotspotHelper.register(options: nil, queue: queue) { (command) in + print(command) + + if(command.commandType == NEHotspotHelperCommandType.filterScanList) { + if let networkList = command.networkList{ + for network in networkList{ + print(network.ssid) + } + } + } + } + return false + + } + + + // Message Dailog + func showMessage(title:String, message:String){ + DispatchQueue.main.async { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert ) + alert.addAction(UIAlertAction(title: "OK", style: .destructive, handler: nil)) + self.present(alert, animated: true) { + + } + } + } + + // Register Geofence + func registerHmgGeofences(result: @escaping FlutterResult){ + flutterMethodChannel?.invokeMethod("getGeofencePreferenceKey", arguments: nil){ geoFencesJsonString in + if let jsonString = geoFencesJsonString as? String{ + let allZones = GeoZoneModel.list(from: jsonString) + HMG_Geofence().register(geoZones: allZones) + + }else{ + } + } + } + +} diff --git a/ios/Runner/Helper/API.swift b/ios/Runner/Helper/API.swift new file mode 100644 index 0000000..b487f03 --- /dev/null +++ b/ios/Runner/Helper/API.swift @@ -0,0 +1,22 @@ +// +// API.swift +// Runner +// +// Created by ZiKambrani on 04/04/1442 AH. +// + +import UIKit + +fileprivate let DOMAIN = "https://uat.hmgwebservices.com" +fileprivate let SERVICE = "Services/Patients.svc/REST" +fileprivate let BASE_URL = "\(DOMAIN)/\(SERVICE)" + +struct API { + static let WIFI_CREDENTIALS = "\(BASE_URL)/Hmg_SMS_Get_By_ProjectID_And_PatientID" +} + + +//struct API { +// static let WIFI_CREDENTIALS = FlutterConstants.WIFI_CREDENTIALS_URL +// static let LOG_GEOFENCE = FlutterConstants.LOG_GEOFENCE_URL +//} diff --git a/ios/Runner/Helper/Extensions.swift b/ios/Runner/Helper/Extensions.swift new file mode 100644 index 0000000..de67f9b --- /dev/null +++ b/ios/Runner/Helper/Extensions.swift @@ -0,0 +1,150 @@ +// +// Extensions.swift +// Runner +// +// Created by ZiKambrani on 04/04/1442 AH. +// + +import UIKit + + +extension String{ + func toUrl() -> URL?{ + return URL(string: self) + } + + func removeSpace() -> String?{ + return self.replacingOccurrences(of: " ", with: "") + } +} + +extension Date{ + func toString(format:String) -> String{ + let df = DateFormatter() + df.dateFormat = format + return df.string(from: self) + } +} + +extension Dictionary{ + func merge(dict:[String:Any?]) -> [String:Any?]{ + var self_ = self as! [String:Any?] + dict.forEach { (kv) in + self_.updateValue(kv.value, forKey: kv.key) + } + return self_ + } +} + +extension Bundle { + + func certificate(named name: String) -> SecCertificate { + let cerURL = self.url(forResource: name, withExtension: "cer")! + let cerData = try! Data(contentsOf: cerURL) + let cer = SecCertificateCreateWithData(nil, cerData as CFData)! + return cer + } + + func identity(named name: String, password: String) -> SecIdentity { + let p12URL = self.url(forResource: name, withExtension: "p12")! + let p12Data = try! Data(contentsOf: p12URL) + + var importedCF: CFArray? = nil + let options = [kSecImportExportPassphrase as String: password] + let err = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &importedCF) + precondition(err == errSecSuccess) + let imported = importedCF! as NSArray as! [[String:AnyObject]] + precondition(imported.count == 1) + + return (imported[0][kSecImportItemIdentity as String]!) as! SecIdentity + } + + +} + +extension SecCertificate{ + func trust() -> Bool?{ + var optionalTrust: SecTrust? + let policy = SecPolicyCreateBasicX509() + + let status = SecTrustCreateWithCertificates([self] as AnyObject, + policy, + &optionalTrust) + guard status == errSecSuccess else { return false} + let trust = optionalTrust! + + let stat = optionalTrust?.evaluateAllowing(rootCertificates: [self]) + return stat + } + + func secTrustObject() -> SecTrust?{ + var optionalTrust: SecTrust? + let policy = SecPolicyCreateBasicX509() + + let status = SecTrustCreateWithCertificates([self] as AnyObject, + policy, + &optionalTrust) + return optionalTrust + } +} + + +extension SecTrust { + + func evaluate() -> Bool { + var trustResult: SecTrustResultType = .invalid + let err = SecTrustEvaluate(self, &trustResult) + guard err == errSecSuccess else { return false } + return [.proceed, .unspecified].contains(trustResult) + } + + func evaluateAllowing(rootCertificates: [SecCertificate]) -> Bool { + + // Apply our custom root to the trust object. + + var err = SecTrustSetAnchorCertificates(self, rootCertificates as CFArray) + guard err == errSecSuccess else { return false } + + // Re-enable the system's built-in root certificates. + + err = SecTrustSetAnchorCertificatesOnly(self, false) + guard err == errSecSuccess else { return false } + + // Run a trust evaluation and only allow the connection if it succeeds. + + return self.evaluate() + } +} + + +extension UIView{ + func show(){ + self.alpha = 0.0 + self.isHidden = false + UIView.animate(withDuration: 0.25, animations: { + self.alpha = 1 + }) { (complete) in + + } + } + + func hide(){ + UIView.animate(withDuration: 0.25, animations: { + self.alpha = 0.0 + }) { (complete) in + self.isHidden = true + } + } +} + + +extension UIViewController{ + func showAlert(withTitle: String, message: String){ + let alert = UIAlertController(title: withTitle, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .destructive, handler: nil)) + present(alert, animated: true) { + + } + } +} + diff --git a/ios/Runner/Helper/FlutterConstants.swift b/ios/Runner/Helper/FlutterConstants.swift new file mode 100644 index 0000000..f1b3f09 --- /dev/null +++ b/ios/Runner/Helper/FlutterConstants.swift @@ -0,0 +1,36 @@ +// +// FlutterConstants.swift +// Runner +// +// Created by ZiKambrani on 22/12/2020. +// + +import UIKit + +class FlutterConstants{ + static var LOG_GEOFENCE_URL:String? + static var WIFI_CREDENTIALS_URL:String? + static var DEFAULT_HTTP_PARAMS:[String:Any?]? + + class func set(){ + + // (FiX) Take a start with FlutterMethodChannel (kikstart) + /* First call to flutter method is not returning the correct value (Always returning 'NSObject') then after it wroking fine and returning correct value*/ + FlutterText.with(key: "test") { (test) in + + flutterMethodChannel?.invokeMethod("getDefaultHttpParameters", arguments: nil){ (response) in + if let defaultHTTPParams = response as? [String:Any?]{ + DEFAULT_HTTP_PARAMS = defaultHTTPParams + } + + } + + flutterMethodChannel?.invokeMethod("getLogGeofenceFullUrl", arguments:nil){ (response) in + if let url = response as? String{ + LOG_GEOFENCE_URL = url + } + } + + } + } +} diff --git a/ios/Runner/Helper/GeoZoneModel.swift b/ios/Runner/Helper/GeoZoneModel.swift new file mode 100644 index 0000000..e703b64 --- /dev/null +++ b/ios/Runner/Helper/GeoZoneModel.swift @@ -0,0 +1,67 @@ +// +// GeoZoneModel.swift +// Runner +// +// Created by ZiKambrani on 13/12/2020. +// + +import UIKit +import CoreLocation + +class GeoZoneModel{ + var geofenceId:Int = -1 + var description:String = "" + var descriptionN:String? + var latitude:String? + var longitude:String? + var radius:Int? + var type:Int? + var projectID:Int? + var imageURL:String? + var isCity:String? + + func identifier() -> String{ + return "\(geofenceId)_hmg" + } + + func message() -> String{ + return description + } + + func toRegion(locationManager:CLLocationManager) -> CLCircularRegion?{ + if let rad = radius, let lat = latitude?.removeSpace(), let long = longitude?.removeSpace(), + let radius_d = Double("\(rad)"), let lat_d = Double(lat), let long_d = Double(long){ + + let coordinate = CLLocationCoordinate2D(latitude: lat_d, longitude: long_d) + let validatedRadius = min(radius_d, locationManager.maximumRegionMonitoringDistance) + + let region = CLCircularRegion(center: coordinate, radius: validatedRadius, identifier: identifier()) + region.notifyOnExit = true + region.notifyOnEntry = true + return region + } + return nil + } + + class func from(json:[String:Any]) -> GeoZoneModel{ + let model = GeoZoneModel() + model.geofenceId = json["GEOF_ID"] as? Int ?? 0 + model.radius = json["Radius"] as? Int + model.projectID = json["ProjectID"] as? Int + model.type = json["Type"] as? Int + model.description = json["Description"] as? String ?? "" + model.descriptionN = json["DescriptionN"] as? String + model.latitude = json["Latitude"] as? String + model.longitude = json["Longitude"] as? String + model.imageURL = json["ImageURL"] as? String + model.isCity = json["IsCity"] as? String + + return model + } + + class func list(from jsonString:String) -> [GeoZoneModel]{ + let value = dictionaryArray(from: jsonString) + let geoZones = value.map { GeoZoneModel.from(json: $0) } + return geoZones + } +} diff --git a/ios/Runner/Helper/GlobalHelper.swift b/ios/Runner/Helper/GlobalHelper.swift new file mode 100644 index 0000000..3768780 --- /dev/null +++ b/ios/Runner/Helper/GlobalHelper.swift @@ -0,0 +1,119 @@ +// +// GlobalHelper.swift +// Runner +// +// Created by ZiKambrani on 29/03/1442 AH. +// + +import UIKit + +func dictionaryArray(from:String) -> [[String:Any]]{ + if let data = from.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] ?? [] + } catch { + print(error.localizedDescription) + } + } + return [] + +} + +func dictionary(from:String) -> [String:Any]?{ + if let data = from.data(using: .utf8) { + do { + return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + +} + +let HmgLocalNotificationCategoryIdentifier = "hmg.local.notification" +func showNotification(identifier:String? = nil, title:String?, subtitle:String?, message:String?, sound:UNNotificationSound = UNNotificationSound.default, categoryIdentifier:String = HmgLocalNotificationCategoryIdentifier){ + DispatchQueue.main.async { + let notificationContent = UNMutableNotificationContent() + notificationContent.categoryIdentifier = categoryIdentifier + + if identifier != nil { notificationContent.categoryIdentifier = identifier! } + if title != nil { notificationContent.title = title! } + if subtitle != nil { notificationContent.body = message! } + if message != nil { notificationContent.subtitle = subtitle! } + + notificationContent.sound = UNNotificationSound.default + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: "\(Date().timeIntervalSinceNow)", content: notificationContent, trigger: trigger) + + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("Error: \(error)") + } + } + } +} + +func appLanguageCode() -> Int{ + let lang = UserDefaults.standard.string(forKey: "language") ?? "ar" + return lang == "ar" ? 2 : 1 +} + +func userProfile() -> [String:Any?]?{ + var userProf = UserDefaults.standard.string(forKey: "flutter.imei-user-data") + if(userProf == nil){ + userProf = UserDefaults.standard.string(forKey: "flutter.user-profile") + } + return dictionary(from: userProf ?? "{}") +} + +fileprivate let defaultHTTPParams:[String : Any?] = [ + "ZipCode" : "966", + "VersionID" : 5.8, + "Channel" : 3, + "LanguageID" : appLanguageCode(), + "IPAdress" : "10.20.10.20", + "generalid" : "Cs2020@2016$2958", + "PatientOutSA" : 0, + "SessionID" : nil, + "isDentalAllowedBackend" : false, + "DeviceTypeID" : 2 +] + +func httpPostRequest(urlString:String, jsonBody:[String:Any?], completion:((Bool,[String:Any]?)->Void)?){ + var json: [String: Any?] = jsonBody + json = json.merge(dict: defaultHTTPParams) + let jsonData = try? JSONSerialization.data(withJSONObject: json) + + // create post request + let url = URL(string: urlString)! + var request = URLRequest(url: url) + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue("*/*", forHTTPHeaderField: "Accept") + request.httpMethod = "POST" + request.httpBody = jsonData + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, error == nil else { + print(error?.localizedDescription ?? "No data") + return + } + + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + if let responseJSON = responseJSON as? [String: Any], let status = responseJSON["MessageStatus"] as? Int{ + print(responseJSON) + if status == 1{ + completion?(true,responseJSON) + }else{ + completion?(false,responseJSON) + } + + }else{ + completion?(false,nil) + } + } + + task.resume() + +} diff --git a/ios/Runner/Helper/HMGPenguinInPlatformBridge.swift b/ios/Runner/Helper/HMGPenguinInPlatformBridge.swift new file mode 100644 index 0000000..db02e8f --- /dev/null +++ b/ios/Runner/Helper/HMGPenguinInPlatformBridge.swift @@ -0,0 +1,94 @@ +import Foundation +import FLAnimatedImage + + +var flutterMethodChannelPenguinIn:FlutterMethodChannel? = nil +fileprivate var mainViewController:FlutterViewController! + +class HMGPenguinInPlatformBridge{ + + private let channelName = "launch_penguin_ui" + private static var shared_:HMGPenguinInPlatformBridge? + + class func initialize(flutterViewController:FlutterViewController){ + shared_ = HMGPenguinInPlatformBridge() + mainViewController = flutterViewController + shared_?.openChannel() + } + + func shared() -> HMGPenguinInPlatformBridge{ + assert((HMGPenguinInPlatformBridge.shared_ != nil), "HMGPenguinInPlatformBridge is not initialized, call initialize(mainViewController:MainFlutterVC) function first.") + return HMGPenguinInPlatformBridge.shared_! + } + + private func openChannel(){ + flutterMethodChannelPenguinIn = FlutterMethodChannel(name: channelName, binaryMessenger: mainViewController.binaryMessenger) + + flutterMethodChannelPenguinIn?.setMethodCallHandler { (methodCall, result) in + print("Called function \(methodCall.method)") + + if let arguments = methodCall.arguments as Any? { + if methodCall.method == "launchPenguin"{ + print("====== launchPenguinView Launched =========") + self.launchPenguinView(arguments: arguments, result: result) + } + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Storyboard name is required", details: nil)) + } + } + } + + private func launchPenguinView(arguments: Any, result: @escaping FlutterResult) { + + let penguinView = PenguinView( + frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height), + viewIdentifier: 0, + arguments: arguments, + binaryMessenger: mainViewController.binaryMessenger + ) + + let penguinUIView = penguinView.view() + penguinUIView.frame = mainViewController.view.bounds + penguinUIView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + mainViewController.view.addSubview(penguinUIView) + + let args = arguments as? [String: Any] +// let loaderImageData = args["loaderImage"] as? FlutterStandardTypedData else { +// print("loaderImage data not found in arguments") +// result(FlutterError(code: "ARGUMENT_ERROR", message: "Missing loaderImage data", details: nil)) +// return +// } + +// let loadingOverlay = UIView(frame: UIScreen.main.bounds) +// loadingOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.5) // Semi-transparent overlay +// loadingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + // Display the GIF using FLAnimatedImage +// let animatedImage = FLAnimatedImage(animatedGIFData: loaderImageData.data) +// let gifImageView = FLAnimatedImageView() +// gifImageView.animatedImage = animatedImage +// gifImageView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) +// gifImageView.center = loadingOverlay.center +// gifImageView.contentMode = .scaleAspectFit +// loadingOverlay.addSubview(gifImageView) + + +// if let window = UIApplication.shared.windows.first { +// window.addSubview(loadingOverlay) +// +// } else { +// print("Error: Main window not found") +// } + + penguinView.onSuccess = { + // Hide and remove the loader +// DispatchQueue.main.async { +// loadingOverlay.removeFromSuperview() +// +// } + } + + result(nil) + } +} diff --git a/ios/Runner/Helper/HMGPlatformBridge.swift b/ios/Runner/Helper/HMGPlatformBridge.swift new file mode 100644 index 0000000..fd9fb40 --- /dev/null +++ b/ios/Runner/Helper/HMGPlatformBridge.swift @@ -0,0 +1,140 @@ +// +// HMGPlatformBridge.swift +// Runner +// +// Created by ZiKambrani on 14/12/2020. +// + +import UIKit +import NetworkExtension +import SystemConfiguration.CaptiveNetwork + +var flutterMethodChannel:FlutterMethodChannel? = nil +fileprivate var mainViewController:MainFlutterVC! + +class HMGPlatformBridge{ + private let channelName = "HMG-Platform-Bridge" + private static var shared_:HMGPlatformBridge? + + class func initialize(flutterViewController:MainFlutterVC){ + shared_ = HMGPlatformBridge() + mainViewController = flutterViewController + shared_?.openChannel() + } + + func shared() -> HMGPlatformBridge{ + assert((HMGPlatformBridge.shared_ != nil), "HMGPlatformBridge is not initialized, call initialize(mainViewController:MainFlutterVC) function first.") + return HMGPlatformBridge.shared_! + } + + private func openChannel(){ + flutterMethodChannel = FlutterMethodChannel(name: channelName, binaryMessenger: mainViewController.binaryMessenger) + flutterMethodChannel?.setMethodCallHandler { (methodCall, result) in + print("Called function \(methodCall.method)") + if methodCall.method == "connectHMGInternetWifi"{ + self.connectHMGInternetWifi(methodCall:methodCall, result: result) + + }else if methodCall.method == "connectHMGGuestWifi"{ + self.connectHMGGuestWifi(methodCall:methodCall, result: result) + + }else if methodCall.method == "isHMGNetworkAvailable"{ + self.isHMGNetworkAvailable(methodCall:methodCall, result: result) + + }else if methodCall.method == "registerHmgGeofences"{ + self.registerHmgGeofences(result: result) + + }else if methodCall.method == "unRegisterHmgGeofences"{ + self.unRegisterHmgGeofences(result: result) + } + + print("") + } + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { (timer) in + FlutterConstants.set() + } + } + + + + // Connect HMG Wifi and Internet + func connectHMGInternetWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + + guard let pateintId = (methodCall.arguments as? [Any])?.first as? String + else { return assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") } + + + HMG_Internet.shared.connect(patientId: pateintId) { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + // Connect HMG-Guest for App Access + func connectHMGGuestWifi(methodCall:FlutterMethodCall ,result: @escaping FlutterResult){ + HMG_GUEST.shared.connect() { (status, message) in + result(status ? 1 : 0) + if status{ + self.showMessage(title:"Congratulations", message:message) + }else{ + self.showMessage(title:"Ooops,", message:message) + } + } + } + + func isHMGNetworkAvailable(methodCall:FlutterMethodCall ,result: @escaping FlutterResult) -> Bool{ + guard let ssid = methodCall.arguments as? String else { + assert(true, "Missing or invalid arguments (Must have one argument 'String at 0'") + return false + } + + let queue = DispatchQueue.init(label: "com.hmg.wifilist") + NEHotspotHelper.register(options: nil, queue: queue) { (command) in + print(command) + + if(command.commandType == NEHotspotHelperCommandType.filterScanList) { + if let networkList = command.networkList{ + for network in networkList{ + print(network.ssid) + } + } + } + } + return false + + } + + + // Message Dailog + func showMessage(title:String, message:String){ + DispatchQueue.main.async { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert ) + alert.addAction(UIAlertAction(title: "OK", style: .destructive, handler: nil)) + mainViewController.present(alert, animated: true) { + + } + } + } + + // Register Geofence + func registerHmgGeofences(result: @escaping FlutterResult){ + flutterMethodChannel?.invokeMethod("getGeoZones", arguments: nil){ geoFencesJsonString in + if let jsonString = geoFencesJsonString as? String{ + let allZones = GeoZoneModel.list(from: jsonString) + HMG_Geofence.shared().register(geoZones: allZones) + result(true) + }else{ + } + } + } + + // Register Geofence + func unRegisterHmgGeofences(result: @escaping FlutterResult){ + HMG_Geofence.shared().unRegisterAll() + result(true) + } + +} diff --git a/ios/Runner/Helper/HMG_Geofence.swift b/ios/Runner/Helper/HMG_Geofence.swift new file mode 100644 index 0000000..47454d3 --- /dev/null +++ b/ios/Runner/Helper/HMG_Geofence.swift @@ -0,0 +1,183 @@ +// +// HMG_Geofence.swift +// Runner +// +// Created by ZiKambrani on 13/12/2020. +// + +import UIKit +import CoreLocation + +fileprivate var df = DateFormatter() +fileprivate var transition = "" + +enum Transition:Int { + case entry = 1 + case exit = 2 + func name() -> String{ + return self.rawValue == 1 ? "Enter" : "Exit" + } +} + +class HMG_Geofence:NSObject{ + + var geoZones:[GeoZoneModel]? + var locationManager:CLLocationManager!{ + didSet{ + // https://developer.apple.com/documentation/corelocation/cllocationmanager/1423531-startmonitoringsignificantlocati + + locationManager.allowsBackgroundLocationUpdates = true + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + locationManager.activityType = .other + locationManager.delegate = self + locationManager.requestAlwaysAuthorization() + // locationManager.distanceFilter = 500 + // locationManager.startMonitoringSignificantLocationChanges() + } + } + + private static var shared_:HMG_Geofence? + class func shared() -> HMG_Geofence{ + if HMG_Geofence.shared_ == nil{ + HMG_Geofence.initGeofencing() + } + return shared_! + } + + class func initGeofencing(){ + shared_ = HMG_Geofence() + shared_?.locationManager = CLLocationManager() + } + + func register(geoZones:[GeoZoneModel]){ + + self.geoZones = geoZones + + let monitoredRegions_ = monitoredRegions() + self.geoZones?.forEach({ (zone) in + if let region = zone.toRegion(locationManager: locationManager){ + if let already = monitoredRegions_.first(where: {$0.identifier == zone.identifier()}){ + debugPrint("Already monitering region: \(already)") + }else{ + startMonitoring(region: region) + } + }else{ + debugPrint("Invalid region: \(zone.latitude ?? "invalid_latitude"),\(zone.longitude ?? "invalid_longitude"),r\(zone.radius ?? 0) | \(zone.identifier())") + } + }) + } + + func monitoredRegions() -> Set{ + return locationManager.monitoredRegions + } + + func unRegisterAll(){ + for region in locationManager.monitoredRegions { + locationManager.stopMonitoring(for: region) + } + } + +} + +// CLLocationManager Delegates +extension HMG_Geofence : CLLocationManagerDelegate{ + + func startMonitoring(region: CLCircularRegion) { + if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) { + return + } + + if CLLocationManager.authorizationStatus() != .authorizedAlways { + let message = """ + Your geotification is saved but will only be activated once you grant + HMG permission to access the device location. + """ + debugPrint(message) + } + + locationManager.startMonitoring(for: region) + locationManager.requestState(for: region) + debugPrint("Starts monitering region: \(region)") + } + + func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { + debugPrint("didEnterRegion: \(region)") + if region is CLCircularRegion { + handleEvent(for: region,transition: .entry, location: manager.location) + } + } + + func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { + debugPrint("didExitRegion: \(region)") + if region is CLCircularRegion { + handleEvent(for: region,transition: .exit, location: manager.location) + } + } + + func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) { + debugPrint("didDetermineState: \(state)") + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + debugPrint("didUpdateLocations: \(locations)") + } + + +} + +// Helpers +extension HMG_Geofence{ + + func handleEvent(for region: CLRegion!, transition:Transition, location:CLLocation?) { + if let userProfile = userProfile(){ + notifyUser(forRegion: region, transition: transition, location: locationManager.location, userProfile: userProfile) + notifyServer(forRegion: region, transition: transition, location: locationManager.location, userProfile: userProfile) + } + } + + func geoZone(by id: String) -> GeoZoneModel? { + var zone:GeoZoneModel? = nil + if let zones_ = geoZones{ + zone = zones_.first(where: { $0.identifier() == id}) + }else{ + // let jsonArray = UserDefaults.standard.string(forKey: "hmg-geo-fences") + } + return zone + } + + + func notifyUser(forRegion:CLRegion, transition:Transition, location:CLLocation?, userProfile:[String:Any?]){ + if let patientId = userProfile["PatientID"] as? Int{ + + } + } + + func notifyServer(forRegion:CLRegion, transition:Transition, location:CLLocation?, userProfile:[String:Any?]){ + if let patientId = userProfile["PatientID"] as? Int{ + + if let idString = forRegion.identifier.split(separator: "_").first, let idInt = Int(idString){ + let body:[String:Any] = [ + "PointsID":idInt, + "GeoType":transition.rawValue, + "PatientID":patientId + ] + + var logs = UserDefaults.init(suiteName: "GeoFenceLog")?.dictionary(forKey: "GEOFENCE_LOGS") ?? [:] + var geo = (logs[forRegion.identifier] as? [String]) ?? [] + + let url = "https://hmgwebservices.com/Services/Patients.svc/REST/GeoF_InsertPatientFileInfo" + httpPostRequest(urlString: url, jsonBody: body){ (status,json) in + let status_ = status ? "Notified successfully:" : "Failed to notify:" + showNotification(title: transition.name(), subtitle: forRegion.identifier, message: status_) + + + geo.append("\(status_) \(transition.name()) at \(Date().toString(format: "dd/MMM/yyy hh:mm:ss"))") + logs.updateValue( geo, forKey: forRegion.identifier) + + UserDefaults.init(suiteName: "GeoFenceLog")?.set(logs, forKey: "GEOFENCE_LOGS") + } + } + } + } +} + diff --git a/ios/Runner/Helper/LocalizedFromFlutter.swift b/ios/Runner/Helper/LocalizedFromFlutter.swift new file mode 100644 index 0000000..8853064 --- /dev/null +++ b/ios/Runner/Helper/LocalizedFromFlutter.swift @@ -0,0 +1,22 @@ +// +// LocalizedFromFlutter.swift +// Runner +// +// Created by ZiKambrani on 10/04/1442 AH. +// + +import UIKit + +class FlutterText{ + + class func with(key:String,completion: @escaping (String)->Void){ + flutterMethodChannel?.invokeMethod("localizedValue", arguments: key, result: { (result) in + if let localized = result as? String{ + completion(localized) + }else{ + completion(key) + } + }) + } + +} diff --git a/ios/Runner/Helper/OpenTokPlatformBridge.swift b/ios/Runner/Helper/OpenTokPlatformBridge.swift new file mode 100644 index 0000000..4da39dc --- /dev/null +++ b/ios/Runner/Helper/OpenTokPlatformBridge.swift @@ -0,0 +1,61 @@ +// +// HMGPlatformBridge.swift +// Runner +// +// Created by ZiKambrani on 14/12/2020. +// + +import UIKit +import NetworkExtension +import SystemConfiguration.CaptiveNetwork + + +fileprivate var openTok:OpenTok? + +class OpenTokPlatformBridge : NSObject{ + private var methodChannel:FlutterMethodChannel? = nil + private var mainViewController:MainFlutterVC! + private static var shared_:OpenTokPlatformBridge? + + class func initialize(flutterViewController:MainFlutterVC, registrar:FlutterPluginRegistrar?){ + shared_ = OpenTokPlatformBridge() + shared_?.mainViewController = flutterViewController + + shared_?.openChannel() + openTok = OpenTok(mainViewController: flutterViewController, registrar: registrar) + } + + func shared() -> OpenTokPlatformBridge{ + assert((OpenTokPlatformBridge.shared_ != nil), "OpenTokPlatformBridge is not initialized, call initialize(mainViewController:MainFlutterVC) function first.") + return OpenTokPlatformBridge.shared_! + } + + private func openChannel(){ + methodChannel = FlutterMethodChannel(name: "OpenTok-Platform-Bridge", binaryMessenger: mainViewController.binaryMessenger) + methodChannel?.setMethodCallHandler { (call, result) in + print("Called function \(call.method)") + + switch(call.method) { + case "initSession": + openTok?.initSession(call: call, result: result) + + case "swapCamera": + openTok?.swapCamera(call: call, result: result) + + case "toggleAudio": + openTok?.toggleAudio(call: call, result: result) + + case "toggleVideo": + openTok?.toggleVideo(call: call, result: result) + + case "hangupCall": + openTok?.hangupCall(call: call, result: result) + + default: + result(FlutterMethodNotImplemented) + } + + print("") + } + } +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8f2ef94..ab9828e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -71,6 +71,8 @@ This app requires contacts access to show incoming virtual consultation request. NSFaceIDUsageDescription This app requires Face ID to allow biometric authentication for app login. + NSHealthClinicalHealthRecordsShareUsageDescription + This App need access to HealthKit to read heart rate & other data from your smart watch. NSHealthShareUsageDescription This App need access to HealthKit to read heart rate & other data from your smart watch. NSHealthUpdateUsageDescription diff --git a/ios/Runner/Penguin/PenguinModel.swift b/ios/Runner/Penguin/PenguinModel.swift new file mode 100644 index 0000000..7b6ab2d --- /dev/null +++ b/ios/Runner/Penguin/PenguinModel.swift @@ -0,0 +1,77 @@ +// +// PenguinModel.swift +// Runner +// +// Created by Amir on 06/08/2024. +// + +import Foundation + +// Define the model class +struct PenguinModel { + let baseURL: String + let dataURL: String + let dataServiceName: String + let positionURL: String + let clientKey: String + let storyboardName: String + let mapBoxKey: String + let clientID: String + let positionServiceName: String + let username: String + let isSimulationModeEnabled: Bool + let isShowUserName: Bool + let isUpdateUserLocationSmoothly: Bool + let isEnableReportIssue: Bool + let languageCode: String + let clinicID: String + let patientID: String + let projectID: Int + + // Initialize the model from a dictionary + init?(from dictionary: [String: Any]) { + + guard + let baseURL = dictionary["baseURL"] as? String, + let dataURL = dictionary["dataURL"] as? String, + let dataServiceName = dictionary["dataServiceName"] as? String, + let positionURL = dictionary["positionURL"] as? String, + let clientKey = dictionary["clientKey"] as? String, + let storyboardName = dictionary["storyboardName"] as? String, + let mapBoxKey = dictionary["mapBoxKey"] as? String, + let clientID = dictionary["clientID"] as? String, + let positionServiceName = dictionary["positionServiceName"] as? String, + let username = dictionary["username"] as? String, + let isSimulationModeEnabled = dictionary["isSimulationModeEnabled"] as? Bool, + let isShowUserName = dictionary["isShowUserName"] as? Bool, + let isUpdateUserLocationSmoothly = dictionary["isUpdateUserLocationSmoothly"] as? Bool, + let isEnableReportIssue = dictionary["isEnableReportIssue"] as? Bool, + let languageCode = dictionary["languageCode"] as? String, + let clinicID = dictionary["clinicID"] as? String, + let patientID = dictionary["patientID"] as? String, + let projectID = dictionary["projectID"] as? Int + else { + print("Initialization failed due to missing or invalid keys.") + return nil + } + + self.baseURL = baseURL + self.dataURL = dataURL + self.dataServiceName = dataServiceName + self.positionURL = positionURL + self.clientKey = clientKey + self.storyboardName = storyboardName + self.mapBoxKey = mapBoxKey + self.clientID = clientID + self.positionServiceName = positionServiceName + self.username = username + self.isSimulationModeEnabled = isSimulationModeEnabled + self.isShowUserName = isShowUserName + self.isUpdateUserLocationSmoothly = isUpdateUserLocationSmoothly + self.isEnableReportIssue = isEnableReportIssue + self.languageCode = languageCode + self.clinicID = clinicID + self.patientID = patientID + self.projectID = projectID + } +} diff --git a/ios/Runner/Penguin/PenguinNavigator.swift b/ios/Runner/Penguin/PenguinNavigator.swift new file mode 100644 index 0000000..31cf626 --- /dev/null +++ b/ios/Runner/Penguin/PenguinNavigator.swift @@ -0,0 +1,57 @@ +import PenNavUI +import UIKit + +class PenguinNavigator { + private var config: PenguinModel + + init(config: PenguinModel) { + self.config = config + } + + private func logError(_ message: String) { + // Centralized logging function + print("PenguinSDKNavigator Error: \(message)") + } + + func navigateToPOI( referenceId:String,completion: @escaping (Bool, String?) -> Void) { + PenNavUIManager.shared.getToken(clientID: config.clientID, clientKey: config.clientKey, showProgress: true) { [weak self] token, error in + + if let error = error { + let errorMessage = "Token error while getting the for Navigate to method" + completion(false, "Failed to get token: \(errorMessage)") + + print("Failed to get token: \(errorMessage)") + return + } + + guard let token = token else { + completion(false, "Token is nil") + print("Token is nil") + return + } + print("Token Generated") + print(token); + + + } + } + + private func handleNavigation(referenceId: String, token: String, completion: @escaping (Bool, String?) -> Void) { + DispatchQueue.main.async { + PenNavUIManager.shared.setToken(token: token) + + PenNavUIManager.shared.navigate(to: referenceId) { [weak self] _, navError in + guard let self = self else { return } + + if let navError = navError { + self.logError("Navigation error: Reference ID invalid") + completion(false, "Navigation error: \(navError.localizedDescription)") + return + } + + // Navigation successful + completion(true, nil) + } + } + } +} diff --git a/ios/Runner/Penguin/PenguinPlugin.swift b/ios/Runner/Penguin/PenguinPlugin.swift new file mode 100644 index 0000000..029bec3 --- /dev/null +++ b/ios/Runner/Penguin/PenguinPlugin.swift @@ -0,0 +1,31 @@ +// +// BlueGpsPlugin.swift +// Runner +// +// Created by Penguin . +// + +//import Foundation +//import Flutter +// +///** +// * A Flutter plugin for integrating Penguin SDK functionality. +// * This class registers a view factory with the Flutter engine to create native views. +// */ +//class PenguinPlugin: NSObject, FlutterPlugin { +// +// /** +// * Registers the plugin with the Flutter engine. +// * +// * @param registrar The [FlutterPluginRegistrar] used to register the plugin. +// * This method is called when the plugin is initialized, and it sets up the communication +// * between Flutter and native code. +// */ +// public static func register(with registrar: FlutterPluginRegistrar) { +// // Create an instance of PenguinViewFactory with the binary messenger from the registrar +// let factory = PenguinViewFactory(messenger: registrar.messenger()) +// +// // Register the view factory with a unique ID for use in Flutter code +// registrar.register(factory, withId: "penguin_native") +// } +//} diff --git a/ios/Runner/Penguin/PenguinView.swift b/ios/Runner/Penguin/PenguinView.swift new file mode 100644 index 0000000..d5303e2 --- /dev/null +++ b/ios/Runner/Penguin/PenguinView.swift @@ -0,0 +1,462 @@ +// + +// BlueGpsView.swift + +// Runner + +// + +// Created by Penguin. + +// + + + +import Foundation +import UIKit +import Flutter +import PenNavUI +import PenguinINRenderer + +import Foundation +import Flutter +import UIKit + + + +/** + + * A custom Flutter platform view for displaying Penguin UI components. + + * This class integrates with the Penguin navigation SDK and handles UI events. + + */ + +class PenguinView: NSObject, FlutterPlatformView, PIEventsDelegate, PenNavInitializationDelegate + +{ + // The main view displayed within the platform view + + private var _view: UIView + + private var model: PenguinModel? + + private var methodChannel: FlutterMethodChannel + + var onSuccess: (() -> Void)? + + + + + + + + /** + + * Initializes the PenguinView with the provided parameters. + + * + + * @param frame The frame of the view, specifying its size and position. + + * @param viewId A unique identifier for this view instance. + + * @param args Optional arguments provided for creating the view. + + * @param messenger The [FlutterBinaryMessenger] used for communication with Dart. + + */ + + init( + + frame: CGRect, + + viewIdentifier viewId: Int64, + + arguments args: Any?, + + binaryMessenger messenger: FlutterBinaryMessenger? + + ) { + + _view = UIView() + + methodChannel = FlutterMethodChannel(name: "launch_penguin_ui", binaryMessenger: messenger!) + + + + super.init() + + + + // Get the screen's width and height to set the view's frame + + let screenWidth = UIScreen.main.bounds.width + + let screenHeight = UIScreen.main.bounds.height + + + + // Uncomment to set the background color of the view + + // _view.backgroundColor = UIColor.red + + + + // Set the frame of the view to cover the entire screen + + _view.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight) + + print("========Inside Penguin View ========") + + print(args) + + guard let arguments = args as? [String: Any] else { + + print("Error: Arguments are not in the expected format.") + + return + + } + + print("===== i got tha Args=======") + + + + // Initialize the model from the arguments + + if let penguinModel = PenguinModel(from: arguments) { + + self.model = penguinModel + + initPenguin(args: penguinModel) + + } else { + + print("Error: Failed to initialize PenguinModel from arguments ") + + } + + // Initialize the Penguin SDK with required configurations + + // initPenguin( arguments: args) + + } + + + + /** + + * Initializes the Penguin SDK with custom configuration settings. + + */ + + func initPenguin(args: PenguinModel) { + +// Set the initialization delegate to handle SDK initialization events + + PenNavUIManager.shared.initializationDelegate = self + + // Configure the Penguin SDK with necessary parameters + + PIRendererSettings.styleUri = "mapbox://styles/rwaid/cm3h30b36007v01qz7ik8a0sk" + + PenNavUIManager.shared + + .setClientKey(args.clientKey) + + .setClientID(args.clientID) + + .setUsername(args.username) + + .setSimulationModeEnabled(isEnable: args.isSimulationModeEnabled) + + .setBaseURL(dataURL: args.dataURL, positionURL: args.positionURL) + + .setServiceName(dataServiceName: args.dataServiceName, positionServiceName: args.positionServiceName) + + .setIsShowUserName(args.isShowUserName) + + .setIsUpdateUserLocationSmoothly(args.isUpdateUserLocationSmoothly) + + .setEnableReportIssue(enable: args.isEnableReportIssue) + + .setLanguage(args.languageCode) + + .setBackButtonVisibility(visible: true) + + .setCampusID(args.projectID) + + .build() + + } + + + + + + /** + + * Returns the main view associated with this platform view. + + * + + * @return The UIView instance that represents this platform view. + + */ + + func view() -> UIView { + + return _view + + } + + + + // MARK: - PIEventsDelegate Methods + + + + + + + + + + /** + + * Called when the Penguin UI is dismissed. + + */ + + func onPenNavUIDismiss() { + + // Handle UI dismissal if needed + + print("====== onPenNavUIDismiss =========") + + self.view().removeFromSuperview() + + } + + + + /** + + * Called when a report issue is generated. + + * + + * @param issue The type of issue reported. + + */ + + func onReportIssue(_ issue: PenNavUI.IssueType) { + + // Handle report issue events if needed + + print("====== onReportIssueError =========") + + methodChannel.invokeMethod("onReportIssue", arguments: ["issueType": issue]) + + + + } + + + + /** + + * Called when the Penguin UI setup is successful. + + */ + +// func onPenNavInitializationSuccess() { +// isInitilized = true +// if let referenceId = referenceId { +// navigator?.navigateToPOI(referenceId: referenceId){ [self] success, errorMessage in +// +// channel?.invokeMethod(PenguinMethod.navigateToPOI.rawValue, arguments: errorMessage) +// +// } +// } +// +// channel?.invokeMethod(PenguinMethod.onPenNavSuccess.rawValue, arguments: nil) +// } + + func onPenNavInitializationSuccess() { + + print("====== onPenNavSuccess =========") + + onSuccess?() + + methodChannel.invokeMethod("onPenNavSuccess", arguments: nil) + + // Obtain the FlutterViewController instance + + let controller: FlutterViewController = UIApplication.shared.windows.first?.rootViewController as! FlutterViewController + + + + print("====== after controller onPenNavSuccess =========") + + _view = UIView(frame: UIScreen.main.bounds) + _view.backgroundColor = .clear + + controller.view.addSubview(_view) + + // Set the events delegate to handle SDK events + + PenNavUIManager.shared.eventsDelegate = self + + + + print("====== after eventsDelegate onPenNavSuccess =========") + + + + // Present the Penguin UI on top of the Flutter view controller + + PenNavUIManager.shared.present(root: controller, view: _view) + + + + + + print("====== after present onPenNavSuccess =========") + + print(model?.clinicID) + + print("====== after present onPenNavSuccess =========") + + + + guard let config = self.model else { + + print("Error: Config Model is nil") + + return + + } + + + + guard let clinicID = self.model?.clinicID, + + let clientID = self.model?.clientID, !clientID.isEmpty else { + + print("Error: Config Client ID is nil or empty") + + return + + } + + + + let navigator = PenguinNavigator(config: config) + + + + PenNavUIManager.shared.getToken(clientID: config.clientID, clientKey: config.clientKey, showProgress: false) { [weak self] token, error in + + if let error = error { + + let errorMessage = "Token error while getting the for Navigate to method" + + print("Failed to get token: \(errorMessage)") + + return + + } + + + + guard let token = token else { + + print("Token is nil") + + return + + } + + print("Token Generated") + + print(token); + + + + self?.handleNavigation(clinicID: clinicID, token: token) { success, errorMessage in + + if success { + + print("Navigation successful") + + } else { + + print("Navigation failed: \(errorMessage ?? "Unknown error")") + + } + + + + } + + + + print("====== after Token onPenNavSuccess =========") + + } + + + + } + + + + + + + + private func handleNavigation(clinicID: String, token: String, completion: @escaping (Bool, String?) -> Void) { + + DispatchQueue.main.async { + + PenNavUIManager.shared.setToken(token: token) + + PenNavUIManager.shared.navigate(to: clinicID) + + completion(true,nil) + + } + + } + + + + + + + + + + /** + + * Called when there is an initialization error with the Penguin UI. + + * + + * @param errorType The type of initialization error. + + * @param errorDescription A description of the error. + + */ + + func onPenNavInitializationError(errorType: PenNavUI.PenNavUIError, errorDescription: String) { + + // Handle initialization errors if needed + + print("onPenNavInitializationErrorType: \(errorType.rawValue)") + + print("onPenNavInitializationError: \(errorDescription)") + } +} diff --git a/ios/Runner/Penguin/PenguinViewFactory.swift b/ios/Runner/Penguin/PenguinViewFactory.swift new file mode 100644 index 0000000..a88bb5d --- /dev/null +++ b/ios/Runner/Penguin/PenguinViewFactory.swift @@ -0,0 +1,59 @@ +// +// BlueGpsViewFactory.swift +// Runner +// +// Created by Penguin . +// + +import Foundation +import Flutter + +/** + * A factory class for creating instances of [PenguinView]. + * This class implements `FlutterPlatformViewFactory` to create and manage native views. + */ +class PenguinViewFactory: NSObject, FlutterPlatformViewFactory { + + // The binary messenger used for communication with the Flutter engine + private var messenger: FlutterBinaryMessenger + + /** + * Initializes the PenguinViewFactory with the given messenger. + * + * @param messenger The [FlutterBinaryMessenger] used to communicate with Dart code. + */ + init(messenger: FlutterBinaryMessenger) { + self.messenger = messenger + super.init() + } + + /** + * Creates a new instance of [PenguinView]. + * + * @param frame The frame of the view, specifying its size and position. + * @param viewId A unique identifier for this view instance. + * @param args Optional arguments provided for creating the view. + * @return An instance of [PenguinView] configured with the provided parameters. + */ + func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return PenguinView( + frame: frame, + viewIdentifier: viewId, + arguments: args, + binaryMessenger: messenger) + } + + /** + * Returns the codec used for encoding and decoding method channel arguments. + * This method is required when `arguments` in `create` is not `nil`. + * + * @return A [FlutterMessageCodec] instance used for serialization. + */ + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } +} diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 319178a..2c37e77 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -4,6 +4,14 @@ aps-environment development + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + health-records + + com.apple.developer.healthkit.background-delivery + com.apple.developer.in-app-payments merchant.com.hmgwebservices diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements new file mode 100644 index 0000000..319178a --- /dev/null +++ b/ios/Runner/RunnerDebug.entitlements @@ -0,0 +1,17 @@ + + + + + aps-environment + development + com.apple.developer.in-app-payments + + merchant.com.hmgwebservices + merchant.com.hmgwebservices.uat + + com.apple.developer.nfc.readersession.formats + + TAG + + + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 888f704..039787b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -19,7 +19,7 @@ abstract class ApiClient { Future post( String endPoint, { - required Map body, + required dynamic body, required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, bool isAllowAny, @@ -27,6 +27,8 @@ abstract class ApiClient { bool isRCService, bool isPaymentServices, bool bypassConnectionCheck, + Map apiHeaders, + bool isBodyPlainText, }); Future get( @@ -89,7 +91,7 @@ class ApiClientImp implements ApiClient { @override post( String endPoint, { - required Map body, + required dynamic body, required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, bool isAllowAny = false, @@ -97,6 +99,8 @@ class ApiClientImp implements ApiClient { bool isRCService = false, bool isPaymentServices = false, bool bypassConnectionCheck = true, + Map? apiHeaders, + bool isBodyPlainText = false, }) async { String url; if (isExternal) { @@ -110,80 +114,84 @@ class ApiClientImp implements ApiClient { } // try { var user = _appState.getAuthenticatedUser(); - Map headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}; - if (!isExternal) { - String? token = _appState.appAuthToken; + Map headers = apiHeaders ?? {'Content-Type': 'application/json', 'Accept': 'application/json'}; - if (body.containsKey('SetupID')) { - body['SetupID'] = body.containsKey('SetupID') ? body['SetupID'] ?? body[''] : SETUP_ID; - } else {} + // When isBodyPlainText is true, skip all body manipulation and use body as-is + if (!isBodyPlainText) { + if (!isExternal) { + String? token = _appState.appAuthToken; - if (body.containsKey('isDentalAllowedBackend')) { - body['isDentalAllowedBackend'] = - body.containsKey('isDentalAllowedBackend') ? body['isDentalAllowedBackend'] ?? IS_DENTAL_ALLOWED_BACKEND : IS_DENTAL_ALLOWED_BACKEND; - } + if (body.containsKey('SetupID')) { + body['SetupID'] = body.containsKey('SetupID') ? body['SetupID'] ?? body[''] : SETUP_ID; + } else {} - if (!body.containsKey('IsPublicRequest')) { - // if (!body.containsKey('PatientType')) { - if (user != null && user.patientType != null) { - body['PatientType'] = user.patientType; - } else { - body['PatientType'] = PATIENT_TYPE.toString(); + if (body.containsKey('isDentalAllowedBackend')) { + body['isDentalAllowedBackend'] = + body.containsKey('isDentalAllowedBackend') ? body['isDentalAllowedBackend'] ?? IS_DENTAL_ALLOWED_BACKEND : IS_DENTAL_ALLOWED_BACKEND; } - if (user != null && user.patientType != null) { - body['PatientTypeID'] = user.patientType; - } else { - body['PatientType'] = PATIENT_TYPE_ID.toString(); - } + if (!body.containsKey('IsPublicRequest')) { + // if (!body.containsKey('PatientType')) { + if (user != null && user.patientType != null) { + body['PatientType'] = user.patientType; + } else { + body['PatientType'] = PATIENT_TYPE.toString(); + } + + if (user != null && user.patientType != null) { + body['PatientTypeID'] = user.patientType; + } else { + body['PatientType'] = PATIENT_TYPE_ID.toString(); + } - if (user != null) { - body['TokenID'] = body['TokenID'] ?? token; + if (user != null) { + body['TokenID'] = body['TokenID'] ?? token; - body['PatientID'] = body['PatientID'] ?? user.patientId; + body['PatientID'] = body['PatientID'] ?? user.patientId; - body['PatientOutSA'] = body.containsKey('PatientOutSA') ? body['PatientOutSA'] ?? user.outSa : user.outSa; - body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe + body['PatientOutSA'] = body.containsKey('PatientOutSA') ? body['PatientOutSA'] ?? user.outSa : user.outSa; + body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe + } + // else { + // body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe + // + // } } - // else { - // body['SessionID'] = body['TokenID'] == null ? ApiConsts.sessionID : getSessionId(body['TokenID'] ?? ""); //getSe - // - // } } - } - // request.versionID = VERSION_ID; - // request.channel = CHANNEL; - // request.iPAdress = IP_ADDRESS; - // request.generalid = GENERAL_ID; - // request.languageID = (languageID == 'ar' ? 1 : 2); - // request.patientOutSA = (request.zipCode == '966' || request.zipCode == '+966') ? 0 : 1; - - // body['VersionID'] = ApiConsts.appVersionID.toString(); - if (!isExternal) { - body['VersionID'] = ApiConsts.appVersionID.toString(); - body['Channel'] = ApiConsts.appChannelId.toString(); - body['IPAdress'] = ApiConsts.appIpAddress; - body['generalid'] = ApiConsts.appGeneralId; - - body['LanguageID'] = _appState.getLanguageID().toString(); - body['Latitude'] = _appState.userLat.toString(); - body['Longitude'] = _appState.userLong.toString(); - body['DeviceTypeID'] = _appState.deviceTypeID; - if (_appState.appAuthToken.isNotEmpty) { - body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; + // request.versionID = VERSION_ID; + // request.channel = CHANNEL; + // request.iPAdress = IP_ADDRESS; + // request.generalid = GENERAL_ID; + // request.languageID = (languageID == 'ar' ? 1 : 2); + // request.patientOutSA = (request.zipCode == '966' || request.zipCode == '+966') ? 0 : 1; + + // body['VersionID'] = ApiConsts.appVersionID.toString(); + if (!isExternal) { + body['VersionID'] = ApiConsts.appVersionID.toString(); + body['Channel'] = ApiConsts.appChannelId.toString(); + body['IPAdress'] = ApiConsts.appIpAddress; + body['generalid'] = ApiConsts.appGeneralId; + + body['LanguageID'] = _appState.getLanguageID().toString(); + body['Latitude'] = _appState.userLat.toString(); + body['Longitude'] = _appState.userLong.toString(); + body['DeviceTypeID'] = _appState.deviceTypeID; + if (_appState.appAuthToken.isNotEmpty) { + body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; + } + + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 1018977; + // body['PatientTypeID'] = 1; + // + // body['PatientOutSA'] = 0; + // body['SessionID'] = "45786230487560q"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; - // body['PatientTypeID'] = 1; - // - // body['PatientOutSA'] = 0; - // body['SessionID'] = "45786230487560q"; + body.removeWhere((key, value) => value == null); } - body.removeWhere((key, value) => value == null); - final bool networkStatus = await Utils.checkConnection(bypassConnectionCheck: bypassConnectionCheck); if (!networkStatus) { @@ -196,12 +204,13 @@ class ApiClientImp implements ApiClient { return; } - final response = await http.post(Uri.parse(url.trim()), body: json.encode(body), headers: headers); + // Handle body encoding based on isBodyPlainText flag + final dynamic requestBody = isBodyPlainText ? body : json.encode(body); + final response = await http.post(Uri.parse(url.trim()), body: requestBody, headers: headers); final int statusCode = response.statusCode; log("uri: ${Uri.parse(url.trim())}"); log("body: ${json.encode(body)}"); - // log("response.body: ${response.body}"); - // log("response.body: ${response.body}"); + log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); logApiEndpointError(endPoint, 'Error While Fetching data', statusCode); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 5499787..07f9ee5 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -14,8 +14,8 @@ var PACKAGES_ORDERS = '/api/orders'; var PACKAGES_ORDER_HISTORY = '/api/orders/items'; var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara'; // var BASE_URL = 'http://10.50.100.198:2018/'; -var BASE_URL = 'https://uat.hmgwebservices.com/'; -// var BASE_URL = 'https://hmgwebservices.com/'; +// var BASE_URL = 'https://uat.hmgwebservices.com/'; +var BASE_URL = 'https://hmgwebservices.com/'; // var BASE_URL = 'http://10.201.204.103/'; // var BASE_URL = 'https://orash.cloudsolutions.com.sa/'; // var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/'; @@ -334,6 +334,8 @@ var GET_PATIENT_SHARE_FOR_WALKIN_APPOINTMENT = 'Services/Doctors.svc/REST/GetChe var CAN_PAY_FOR_FOR_WALKIN_APPOINTMENT = 'Services/Doctors.svc/REST/CanPayForWalkinAppointment'; +var GET_APPOINTMENT_NEAREST_GATE = 'Services/OUTPs.svc/REST/getGateByProjectIDandClinicID'; + //URL to get medicine and pharmacies list var CHANNEL = 3; var GENERAL_ID = 'Cs2020@2016\$2958'; @@ -396,19 +398,6 @@ var GET_COVID_DRIVETHRU_PROCEDURES_LIST = 'Services/Doctors.svc/REST/COVID19_Get var GET_PATIENT_LAST_RECORD = 'Services/Patients.svc/REST/Med_GetPatientLastRecord'; var INSERT_PATIENT_HEALTH_DATA = 'Services/Patients.svc/REST/Med_InsertTransactions'; -///My Trackers -var GET_DIABETIC_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_GetDiabeticResultAverage'; -var GET_DIABTEC_RESULT = 'Services/Patients.svc/REST/Patient_GetDiabtecResults'; -var ADD_DIABTEC_RESULT = 'Services/Patients.svc/REST/Patient_AddDiabtecResult'; - -var GET_BLOOD_PRESSURE_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_GetBloodPressureResultAverage'; -var GET_BLOOD_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_GetBloodPressureResult'; -var ADD_BLOOD_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_AddBloodPressureResult'; - -var GET_WEIGHT_PRESSURE_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResultAverage'; -var GET_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResult'; -var ADD_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_AddWeightMeasurementResult'; - var ADD_ACTIVE_PRESCRIPTIONS_REPORT_BY_PATIENT_ID = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; var GET_CALL_INFO_HOURS_RESULT = 'Services/Doctors.svc/REST/GetCallInfoHoursResult'; @@ -418,7 +407,6 @@ var GET_QUESTION_TYPES = 'Services/OUTPs.svc/REST/getQuestionsTypes'; var UPDATE_DIABETIC_RESULT = 'Services/Patients.svc/REST/Patient_UpdateDiabeticResult'; -var SEND_AVERAGE_BLOOD_SUGAR_REPORT = 'Services/Notifications.svc/REST/SendAverageBloodSugarReport'; var DEACTIVATE_DIABETIC_STATUS = 'services/Patients.svc/REST/Patient_DeactivateDiabeticStatus'; var DEACTIVATE_BLOOD_PRESSURES_STATUS = 'services/Patients.svc/REST/Patient_DeactivateBloodPressureStatus'; @@ -437,14 +425,6 @@ var RATE_DOCTOR_RESPONSE = 'Services/OUTPs.svc/REST/insertAppointmentQuestionRat var GET_PATIENT_ALLERGIES = 'Services/Patients.svc/REST/GetPatientAllergies'; -// H2O -var H2O_GET_USER_PROGRESS = "Services/H2ORemainder.svc/REST/H2O_GetUserProgress"; -var H2O_INSERT_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2O_InsertUserActivity"; -var H2O_GET_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_GetUserDetails_New"; -var H2O_UPDATE_USER_DETAIL = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New"; -var H2O_UNDO_USER_ACTIVITY = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity"; -//E_Referral Services - // Encillary Orders var GET_ANCILLARY_ORDERS = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; @@ -454,8 +434,6 @@ var GET_ANCILLARY_ORDERS_DETAILS = 'Services/Doctors.svc/REST/GetOnlineAncillary //Pharmacy wishlist // var GET_WISHLIST = "http://swd-pharapp-01:7200/api/shopping_cart_items/"; -var GET_DOCTOR_LIST_BY_TIME = "Services/Doctors.svc/REST/SearchDoctorsByTime"; - // pharmacy var PHARMACY_AUTORZIE_CUSTOMER = "AutorizeCustomer"; var PHARMACY_VERIFY_CUSTOMER = "VerifyCustomer"; @@ -553,7 +531,6 @@ var GET_FINAL_PRODUCTS = 'products?fields=id,reviews,discount_ids,name,namen,localized_names,display_order,short_description,full_description,full_descriptionn,sku,order_minimum_quantity,order_maximum_quantity,price,old_price,images,is_rx,rx_message,rx_messagen,discount_name,discount_namen,approved_rating_sum,approved_total_reviews,allow_back_in_stock_subscriptions,stock_quantity,stock_availability,stock_availabilityn,discount_percentage&CategoryId='; var GET_CLINIC_CATEGORY = 'Services/Doctors.svc/REST/DP_GetClinicCategory'; var GET_DISEASE_BY_CLINIC_ID = 'Services/Doctors.svc/REST/DP_GetDiseasesByClinicID'; -var SEARCH_DOCTOR_BY_TIME = 'Services/Doctors.svc/REST/SearchDoctorsByTime'; var TIMER_MIN = 10; @@ -673,25 +650,6 @@ var addPayFortApplePayResponse = "Services/PayFort_Serv.svc/REST/AddResponse"; // Auth Provider Consts -const String INSERT_DEVICE_IMEI = 'Services/Patients.svc/REST/Patient_INSERTDeviceIMEI'; -const String SELECT_DEVICE_IMEI = 'Services/Patients.svc/REST/Patient_SELECTDeviceIMEIbyIMEI'; -const String CHECK_PATIENT_AUTH = 'Services/Authentication.svc/REST/CheckPatientAuthentication'; -const GET_MOBILE_INFO = 'Services/Authentication.svc/REST/GetMobileLoginInfo'; - -const FORGOT_PASSWORD = 'Services/Authentication.svc/REST/CheckActivationCodeForSendFileNo'; -const CHECK_PATIENT_FOR_REGISTRATION = "Services/Authentication.svc/REST/CheckPatientForRegisteration"; - -const CHECK_USER_STATUS = "Services/NHIC.svc/REST/GetPatientInfo"; -const REGISTER_USER = 'Services/Authentication.svc/REST/PatientRegistration'; -const LOGGED_IN_USER_URL = 'Services/MobileNotifications.svc/REST/Insert_PatientMobileDeviceInfo'; - -const FORGOT_PATIENT_ID = 'Services/Authentication.svc/REST/SendPatientIDSMSByMobileNumber'; -const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard'; -const PROFILE_SETTING = 'Services/Patients.svc/REST/GetPateintInfoForUpdate'; -const SAVE_SETTING = 'Services/Patients.svc/REST/UpdatePateintInfo'; - -const DEACTIVATE_ACCOUNT = 'Services/Patients.svc/REST/PatientAppleActivation_InsertUpdate'; - var ER_CREATE_ADVANCE_PAYMENT = "services/Doctors.svc/REST/ER_CreateAdvancePaymentForClinic"; //family Files @@ -723,6 +681,8 @@ class ApiConsts { static String GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; static String GET_TAMARA_PAYMENT_STATUS = 'https://mdlaboratories.com/tamaralive/api/OnlineTamara/order_status?orderid='; + static String QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; + // static String GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; // var payFortEnvironment = FortEnvironment.test; @@ -739,6 +699,7 @@ class ApiConsts { GET_TAMARA_INSTALLMENTS_URL = "https://mdlaboratories.com/tamaralive/Home/GetInstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://mdlaboratories.com/tamaralive/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/'; + QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; break; case AppEnvironmentTypeEnum.dev: baseUrl = "https://uat.hmgwebservices.com/"; @@ -749,6 +710,7 @@ class ApiConsts { GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; + QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; @@ -759,6 +721,7 @@ class ApiConsts { GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; + QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; break; case AppEnvironmentTypeEnum.preProd: baseUrl = "https://webservices.hmg.com/"; @@ -769,6 +732,7 @@ class ApiConsts { GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/'; + QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; break; case AppEnvironmentTypeEnum.qa: baseUrl = "https://uat.hmgwebservices.com/"; @@ -779,6 +743,7 @@ class ApiConsts { GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; + QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; break; case AppEnvironmentTypeEnum.staging: baseUrl = "https://uat.hmgwebservices.com/"; @@ -789,6 +754,7 @@ class ApiConsts { GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; + QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; break; } } @@ -845,15 +811,15 @@ class ApiConsts { static final String updateHHCOrder = 'api/hhc/update'; static final String addHHCOrder = 'api/HHC/add'; - // SYMPTOMS CHECKER + // SYMPTOMS CHECKER API + static final String symptomsUserLogin = '$symptomsCheckerApi/user_login'; static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName'; static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors'; - static final String getGeneralSuggestion = '$symptomsCheckerApi/GetGeneralSggestion'; - static final String diagnosis = '$symptomsCheckerApi/diagnosis'; + static final String getSuggestions = '$symptomsCheckerApi/GetSuggestion'; + static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis'; static final String explain = '$symptomsCheckerApi/explain'; //E-REFERRAL SERVICES - static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes"; static final sendActivationCodeForEReferral = 'Services/Authentication.svc/REST/SendActivationCodeForEReferral'; static final checkActivationCodeForEReferral = 'Services/Authentication.svc/REST/CheckActivationCodeForEReferral'; @@ -861,6 +827,45 @@ class ApiConsts { static final createEReferral = "Services/Patients.svc/REST/CreateEReferral"; static final getEReferrals = "Services/Patients.svc/REST/GetEReferrals"; + //WATER CONSUMPTION + static String h2oGetUserProgress = "Services/H2ORemainder.svc/REST/H2O_GetUserProgress"; + static String h2oInsertUserActivity = "Services/H2ORemainder.svc/REST/H2O_InsertUserActivity"; + static String h2oInsertUserDetailsNew = "Services/H2ORemainder.svc/REST/H2O_InsertUserDetails_New"; + static String h2oGetUserDetail = "Services/H2ORemainder.svc/REST/H2O_GetUserDetails_New"; + static String h2oUpdateUserDetail = "Services/H2ORemainder.svc/REST/H2O_UpdateUserDetails_New"; + static String h2oUndoUserActivity = "Services/H2ORemainder.svc/REST/H2o_UndoUserActivity"; + + // HEALTH TRACKERS + // Blood Sugar (Diabetic) + static String getDiabeticResultAverage = 'Services/Patients.svc/REST/Patient_GetDiabeticResultAverage'; + static String getDiabeticResult = 'Services/Patients.svc/REST/Patient_GetDiabtecResults'; + static String addDiabeticResult = 'Services/Patients.svc/REST/Patient_AddDiabtecResult'; + static String updateDiabeticResult = 'Services/Patients.svc/REST/Patient_UpdateDiabtecResult'; + static String deactivateDiabeticStatus = 'Services/Patients.svc/REST/Patient_DeactivateDiabeticStatus'; + static String sendAverageBloodSugarReport = 'Services/Notifications.svc/REST/SendAverageBloodSugarReport'; + + // Blood Pressure + static String getBloodPressureResultAverage = 'Services/Patients.svc/REST/Patient_GetBloodPressureResultAverage'; + static String getBloodPressureResult = 'Services/Patients.svc/REST/Patient_GetBloodPressureResult'; + static String addBloodPressureResult = 'Services/Patients.svc/REST/Patient_AddBloodPressureResult'; + static String updateBloodPressureResult = 'Services/Patients.svc/REST/Patient_UpdateBloodPressureResult'; + static String deactivateBloodPressureStatus = 'Services/Patients.svc/REST/Patient_DeactivateBloodPressureStatus'; + static String sendAverageBloodPressureReport = 'Services/Notifications.svc/REST/SendAverageBloodPressureReport'; + + // Weight Measurement + static String getWeightMeasurementResultAverage = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResultAverage'; + static String getWeightMeasurementResult = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResult'; + static String addWeightMeasurementResult = 'Services/Patients.svc/REST/Patient_AddWeightMeasurementResult'; + static String updateWeightMeasurementResult = 'Services/Patients.svc/REST/Patient_UpdateWeightMeasurementResult'; + static String deactivateWeightMeasurementStatus = 'Services/Patients.svc/REST/Patient_DeactivateWeightMeasurementStatus'; + static String sendAverageBodyWeightReport = 'Services/Notifications.svc/REST/SendAverageBodyWeightReport'; + + //Blood Donation + static String bloodGroupUpdate = "Services/PatientVarification.svc/REST/BloodDonation_RegisterBloodType"; + static String userAgreementForBloodGroupUpdate = "Services/PatientVarification.svc/REST/AddUserAgreementForBloodDonation"; + static String getProjectsHaveBDClinics = "Services/OUTPs.svc/REST/BD_getProjectsHaveBDClinics"; + static String getClinicsBDFreeSlots = "Services/OUTPs.svc/REST/BD_GetFreeSlots"; + // ************ static values for Api **************** static final double appVersionID = 50.3; static final int appChannelId = 3; @@ -872,3 +877,34 @@ class ApiConsts { class ApiKeyConstants { static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng'; } + +//flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_InsertUserActivity +// flutter: {"IdentificationNo":"2530976584","MobileNumber":"504278212","QuantityIntake":200,"VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":200.00,"PercentageConsumed":9.41,"PercentageLeft":90.59,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[{"Quantity":200.000,"CreatedDate":"\/Date(1766911222217+0300)\/"}],"VerificationCode":null,"isSMSSent":false} + +// URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2o_UndoUserActivity +// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} + +// Progress":2 means weekly data + +// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress +// flutter: {"Progress":2,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":null,"UserProgressForWeekData":[{"DayNumber":1,"DayDate":null,"DayName":"Sunday","PercentageConsumed":0},{"DayNumber":7,"DayDate":null,"DayName":"Saturday","PercentageConsumed":0},{"DayNumber":6,"DayDate":null,"DayName":"Friday","PercentageConsumed":0},{"DayNumber":5,"DayDate":null,"DayName":"Thursday","PercentageConsumed":0},{"DayNumber":4,"DayDate":null,"DayName":"Wednesday","PercentageConsumed":0},{"DayNumber":3,"DayDate":null,"DayName":"Tuesday","PercentageConsumed":0},{"DayNumber":2,"DayDate":null,"DayName":"Monday","PercentageConsumed":0}],"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} + +// Progress":1 means daily data + +//URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress +// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} + +// Progress":1 means monthly data + +// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress +// flutter: {"Progress":3,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} +// flutter: response.body: +// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":[{"MonthNumber":1,"MonthName":"January","PercentageConsumed":0},{"MonthNumber":2,"MonthName":"February","PercentageConsumed":0},{"MonthNumber":3,"MonthName":"March","PercentageConsumed":0},{"MonthNumber":4,"MonthName":"April","PercentageConsumed":0},{"MonthNumber":5,"MonthName":"May","PercentageConsumed":0},{"MonthNumber":6,"MonthName":"June","PercentageConsumed":0},{"MonthNumber":7,"MonthName":"July","PercentageConsumed":0},{"MonthNumber":8,"MonthName":"August","PercentageConsumed":0},{"MonthNumber":9,"MonthName":"September","PercentageConsumed":0},{"MonthNumber":10,"MonthName":"October","PercentageConsumed":0},{"MonthNumber":11,"MonthName":"November","PercentageConsumed":0},{"MonthNumber":12,"MonthName":"December","PercentageConsumed":0}],"UserProgressForTodayData":null,"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 3ee41f2..c470bc2 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -188,6 +188,42 @@ class AppAssets { static const String latest_news_icon = '$svgBasePath/latest_news_icon.svg'; static const String hmg_contact_icon = '$svgBasePath/hmg_contact_icon.svg'; static const String services_medical_file_icon = '$svgBasePath/services_medical_file_icon.svg'; + static const String blood_sugar_icon = '$svgBasePath/blood_sugar_icon.svg'; + static const String weight_tracker_icon = '$svgBasePath/weight_tracker_icon.svg'; + static const String ask_doctor_medical_file_icon = '$svgBasePath/ask_doctor_medical_file_icon.svg'; + static const String internet_pairing_icon = '$svgBasePath/internet_pairing_icon.svg'; + static const String my_doctors_icon = '$svgBasePath/my_doctors_icon.svg'; + static const String my_sick_leave_icon = '$svgBasePath/my_sick_leave_icon.svg'; + static const String my_radiology_icon = '$svgBasePath/my_radiology_icon.svg'; + static const String monthly_reports_icon = '$svgBasePath/monthly_reports_icon.svg'; + static const String medical_reports_icon = '$svgBasePath/medical_reports_icon.svg'; + static const String sick_leave_report_icon = '$svgBasePath/sick_leave_report_icon.svg'; + static const String update_insurance_icon = '$svgBasePath/update_insurance_icon.svg'; + static const String insurance_approval_icon = '$svgBasePath/insurance_approval_icon.svg'; + static const String invoices_list_icon = '$svgBasePath/invoices_list_icon.svg'; + static const String ancillary_orders_list_icon = '$svgBasePath/ancillary_orders_list_icon.svg'; + static const String daily_water_monitor_icon = '$svgBasePath/daily_water_monitor.svg'; + static const String health_calculators_services_icon = '$svgBasePath/health_calculators_services_icon.svg'; + static const String health_converters_icon = '$svgBasePath/health_converters_icon.svg'; + static const String smartwatch_icon = '$svgBasePath/smartwatch_icon.svg'; + static const String bmi = '$svgBasePath/bmi.svg'; + static const String bmr = '$svgBasePath/bmr.svg'; + static const String calories = '$svgBasePath/calories.svg'; + static const String ibw = '$svgBasePath/ibw.svg'; + static const String general_health = '$svgBasePath/general_health.svg'; + static const String women_health = '$svgBasePath/women_health.svg'; + + static const String height = '$svgBasePath/height.svg'; + static const String weight = '$svgBasePath/weight.svg'; + static const String activity = '$svgBasePath/activity.svg'; + static const String age = '$svgBasePath/age_icon.svg'; + static const String gender = '$svgBasePath/gender_icon.svg'; + static const String genderInputIcon = '$svgBasePath/genderInputIcon.svg'; + static const String bloodType = '$svgBasePath/blood_type.svg'; + + static const String trade_down_yellow = '$svgBasePath/trade_down_yellow.svg'; + static const String trade_down_red = '$svgBasePath/trade_down_red.svg'; + static const String pharmacy_icon = '$svgBasePath/phramacy_icon.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; @@ -212,10 +248,67 @@ class AppAssets { static const String rotateIcon = '$svgBasePath/rotate_icon.svg'; static const String refreshIcon = '$svgBasePath/refresh.svg'; static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg'; + static const String symptomCheckerIcon = '$svgBasePath/symptom_checker_icon.svg'; + static const String symptomCheckerBottomIcon = '$svgBasePath/symptom_bottom_icon.svg'; + + // Water Monitor + static const String waterBottle = '$svgBasePath/water_bottle.svg'; + static const String cupAdd = '$svgBasePath/cup_add.svg'; + static const String cupFilled = '$svgBasePath/cup_filled.svg'; + static const String waterBottleOuterBubbles = '$svgBasePath/outer_bubbles.svg'; + static const String cupEmpty = '$svgBasePath/cup_empty.svg'; + static const String dumbellIcon = '$svgBasePath/dumbell_icon.svg'; + static const String weightScaleIcon = '$svgBasePath/weight_scale_icon.svg'; + static const String heightIcon = '$svgBasePath/height_icon.svg'; + static const String profileIcon = '$svgBasePath/profile_icon.svg'; + static const String notificationIconGrey = '$svgBasePath/notification_icon_grey.svg'; + static const String minimizeIcon = '$svgBasePath/minimize_icon.svg'; + static const String addIconDark = '$svgBasePath/add_icon_dark.svg'; + static const String glassIcon = '$svgBasePath/glass_icon.svg'; + static const String graphIcon = '$svgBasePath/graph_icon.svg'; + static const String listIcon = '$svgBasePath/list_icon.svg'; + static const String yellowArrowDownIcon = '$svgBasePath/yellow_arrow_down_icon.svg'; + static const String greenTickIcon = '$svgBasePath/green_tick_icon.svg'; + + static const String bloodSugar = '$svgBasePath/bloodsugar.svg'; + static const String bloodCholestrol = '$svgBasePath/bloodcholestrol.svg'; + static const String triglycerides = '$svgBasePath/triglycerides.svg'; + static const String bulb = '$svgBasePath/bulb.svg'; + static const String switchBtn = '$svgBasePath/switch.svg'; + + //Health Trackers + static const String bloodPressureIcon = '$svgBasePath/blood_pressure_icon.svg'; + static const String bloodSugarOnlyIcon = '$svgBasePath/blood_sugar_only_icon.svg'; + static const String weightIcon = '$svgBasePath/weight_icon.svg'; + static const String normalStatusGreenIcon = '$svgBasePath/normal_status_green_icon.svg'; + static const String sendEmailIcon = '$svgBasePath/send_email_icon.svg'; + static const String lowIndicatorIcon = '$svgBasePath/low_indicator_icon.svg'; + + // Health Calculators + static const String ovulationAccordion = '$svgBasePath/approximate_ovulation_accordion.svg'; + static const String nextPeriodAccordion = '$svgBasePath/next_period_accordion.svg'; + static const String fertileAccordion = '$svgBasePath/fertile_window_accordion.svg'; + static const String pregnancyDayAccordion = '$svgBasePath/pregnancy_test_day_accordion.svg'; + static const String pregnancyDueDateAccordion = '$svgBasePath/due_date_accordion.svg'; + + static const String covid19icon = '$svgBasePath/covid_19.svg'; + + //vital sign + static const String heartRate = '$svgBasePath/heart_rate.svg'; + static const String respRate = '$svgBasePath/resp_rate.svg'; + static const String weightVital = '$svgBasePath/weight_2.svg'; + static const String bmiVital = '$svgBasePath/bmi_2.svg'; + static const String heightVital = '$svgBasePath/height_2.svg'; + static const String bloodPressure = '$svgBasePath/blood_pressure.svg'; + static const String temperature = '$svgBasePath/temperature.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; static const String liveCareService = '$pngBasePath/livecare_service.png'; + + static const String homeHealthCareService = '$pngBasePath/home_health_care.png'; + static const String pharmacyService = '$pngBasePath/pharmacy_service.png'; + static const String maleImg = '$pngBasePath/male_img.png'; static const String femaleImg = '$pngBasePath/female_img.png'; static const String babyGirlImg = '$pngBasePath/baby_girl_img.png'; @@ -234,6 +327,7 @@ class AppAssets { static const String fullBodyFront = '$pngBasePath/full_body_front.png'; static const String fullBodyBack = '$pngBasePath/full_body_back.png'; + static const String bmiFullBody = '$pngBasePath/bmi_image_1.png'; } class AppAnimations { diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index c8659a7..e1d6e05 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -4,10 +4,8 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:gms_check/gms_check.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/HMCProjectListModel.dart'; -import 'package:hmg_patient_app_new/core/common_models/privilege/PrivilegeModel.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/ProjectDetailListModel.dart'; import 'package:hmg_patient_app_new/core/common_models/privilege/VidaPlusProjectListModel.dart'; -import 'package:hmg_patient_app_new/features/authentication/models/request_models/send_activation_request_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; @@ -45,7 +43,7 @@ class AppState { bool isChildLoggedIn = false; bool isGMSAvailable = true; bool isAndroid = true; - + bool isRatedVisible =false; void setAuthenticatedUser(AuthenticatedUser? authenticatedUser, {bool isFamily = false}) { if (isFamily) { _authenticatedChildUser = authenticatedUser; @@ -172,4 +170,8 @@ class AppState { userLong = 0.0; userLong = 0.0; } + + setRatedVisible(bool value) { + isRatedVisible = value; + } } diff --git a/lib/core/cache_consts.dart b/lib/core/cache_consts.dart index bcbb185..c1e06aa 100644 --- a/lib/core/cache_consts.dart +++ b/lib/core/cache_consts.dart @@ -63,6 +63,7 @@ class CacheConst { static const String pharmacyAutorzieToken = 'PHARMACY_AUTORZIE_TOKEN'; static const String h2oUnit = 'H2O_UNIT'; static const String h2oReminder = 'H2O_REMINDER'; + static const String waterReminderEnabled = 'WATER_REMINDER_ENABLED'; static const String livecareClinicData = 'LIVECARE_CLINIC_DATA'; static const String doctorScheduleDateSel = 'DOCTOR_SCHEDULE_DATE_SEL'; static const String appointmentHistoryMedical = 'APPOINTMENT_HISTORY_MEDICAL'; @@ -74,6 +75,7 @@ class CacheConst { static const String patientOccupationList = 'patient-occupation-list'; static const String hasEnabledQuickLogin = 'has-enabled-quick-login'; static const String quickLoginEnabled = 'quick-login-enabled'; + static const String isMonthlyReportEnabled = 'is-monthly-report-enabled'; static const String zoomRoomID = 'zoom-room-id'; static String isAppOpenedFromCall = "is_app_opened_from_call"; diff --git a/lib/core/common_models/data_points.dart b/lib/core/common_models/data_points.dart index 3f5065c..f156ecb 100644 --- a/lib/core/common_models/data_points.dart +++ b/lib/core/common_models/data_points.dart @@ -1,26 +1,26 @@ - - ///class used to provide value for the [DynamicResultChart] to plot the values class DataPoint { ///values that is displayed on the graph and dot is plotted on this final double value; + ///label shown on the bottom of the graph String label; String referenceValue; String actualValue; - String? unitOfMeasurement ; + String? unitOfMeasurement; + DateTime time; String displayTime; - DataPoint( - {required this.value, - required this.label, - required this.referenceValue, - required this.actualValue, - required this.time, - required this.displayTime, - this.unitOfMeasurement - }); + DataPoint({ + required this.value, + required this.label, + required this.actualValue, + required this.time, + required this.displayTime, + this.unitOfMeasurement, + this.referenceValue = '', + }); @override String toString() { diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 4c17de6..582b795 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -1,4 +1,5 @@ import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -17,6 +18,7 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_repo.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_repo.dart'; @@ -29,9 +31,14 @@ import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_repo.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; @@ -39,10 +46,14 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -51,11 +62,14 @@ import 'package:hmg_patient_app_new/services/firebase_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + GetIt getIt = GetIt.instance; class AppDependencies { @@ -97,6 +111,13 @@ class AppDependencies { final sharedPreferences = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => CacheServiceImp(sharedPreferences: sharedPreferences, loggerService: getIt())); + + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + getIt.registerLazySingleton(() => NotificationServiceImp( + flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin, + loggerService: getIt(), + )); + getIt.registerLazySingleton(() => ApiClientImp(appState: getIt())); getIt.registerLazySingleton( () => LocalAuthService(loggerService: getIt(), localAuth: getIt()), @@ -122,6 +143,10 @@ class AppDependencies { getIt.registerLazySingleton(() => HmgServicesRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => SymptomsCheckerRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => BloodDonationRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => WaterMonitorRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => MyInvoicesRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => HealthTrackersRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => MonthlyReportRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton @@ -132,26 +157,20 @@ class AppDependencies { () => RadiologyViewModel(radiologyRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()), ); - getIt.registerLazySingleton( - () => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + getIt.registerLazySingleton(() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); getIt.registerLazySingleton(() => InsuranceViewModel(insuranceRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); + getIt.registerLazySingleton(() => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); + + getIt.registerLazySingleton(() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); getIt.registerLazySingleton( - () => PayfortViewModel( - payfortRepo: getIt(), - errorHandlerService: getIt(), - ), + () => PayfortViewModel(payfortRepo: getIt(), errorHandlerService: getIt()), ); getIt.registerLazySingleton( - () => HabibWalletViewModel( - habibWalletRepo: getIt(), - errorHandlerService: getIt(), - ), + () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()), ); getIt.registerLazySingleton( @@ -163,12 +182,7 @@ class AppDependencies { getIt.registerLazySingleton( () => BookAppointmentsViewModel( - bookAppointmentsRepo: getIt(), - errorHandlerService: getIt(), - navigationService: getIt(), - myAppointmentsViewModel: getIt(), - locationUtils: getIt(), - dialogService: getIt()), + bookAppointmentsRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt(), myAppointmentsViewModel: getIt(), locationUtils: getIt(), dialogService: getIt()), ); getIt.registerLazySingleton( @@ -182,14 +196,9 @@ class AppDependencies { getIt.registerLazySingleton( () => AuthenticationViewModel( - authenticationRepo: getIt(), - cacheService: getIt(), - navigationService: getIt(), - dialogService: getIt(), - appState: getIt(), - errorHandlerService: getIt(), - localAuthService: getIt()), + authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()), ); + getIt.registerLazySingleton(() => ProfileSettingsViewModel()); getIt.registerLazySingleton(() => DateRangeSelectorRangeViewModel()); @@ -202,13 +211,14 @@ class AppDependencies { getIt.registerLazySingleton( () => EmergencyServicesViewModel( - locationUtils: getIt(), - navServices: getIt(), - emergencyServicesRepo: getIt(), - appState: getIt(), - errorHandlerService: getIt(), - appointmentRepo: getIt(), - dialogService: getIt()), + locationUtils: getIt(), + navServices: getIt(), + emergencyServicesRepo: getIt(), + appState: getIt(), + errorHandlerService: getIt(), + appointmentRepo: getIt(), + dialogService: getIt(), + ), ); getIt.registerLazySingleton( @@ -219,26 +229,50 @@ class AppDependencies { () => ContactUsViewModel(contactUsRepo: getIt(), appState: getIt(), errorHandlerService: getIt()), ); - getIt.registerLazySingleton( - () => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()), + getIt.registerLazySingleton(() => HealthCalcualtorViewModel()); + + getIt.registerLazySingleton(() => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt())); + + getIt.registerLazySingleton( + () => SymptomsCheckerViewModel( + errorHandlerService: getIt(), + symptomsCheckerRepo: getIt(), + appState: getIt(), + ), ); - getIt.registerLazySingleton(() => SymptomsCheckerViewModel(errorHandlerService: getIt(), symptomsCheckerRepo: getIt())); getIt.registerLazySingleton( - () => HmgServicesViewModel(bookAppointmentsRepo: getIt(), hmgServicesRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()), + () => HmgServicesViewModel( + bookAppointmentsRepo: getIt(), + hmgServicesRepo: getIt(), + errorHandlerService: getIt(), + navigationService: getIt(), + ), ); getIt.registerLazySingleton( - () => BloodDonationViewModel(bloodDonationRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt()), + () => BloodDonationViewModel( + bloodDonationRepo: getIt(), + errorHandlerService: getIt(), + navigationService: getIt(), + dialogService: getIt(), + appState: getIt(), + navServices: getIt(), + ), ); - // Screen-specific VMs → Factory - // getIt.registerFactory( - // () => BookAppointmentsViewModel( - // bookAppointmentsRepo: getIt(), - // dialogService: getIt(), - // errorHandlerService: getIt(), - // ), - // ); + getIt.registerLazySingleton(() => HealthProvider()); + + getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); + + getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + + getIt.registerLazySingleton(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); + getIt.registerLazySingleton(() => MyInvoicesViewModel( + myInvoicesRepo: getIt(), + errorHandlerService: getIt(), + navServices: getIt(), + )); + getIt.registerLazySingleton(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt())); } } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 6754e8c..6dc3bf6 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -16,7 +16,7 @@ enum CountryEnum { saudiArabia, unitedArabEmirates } enum CalenderEnum { gregorian, hijri } -enum SelectionTypeEnum { dropdown, calendar, search } +enum SelectionTypeEnum { dropdown, calendar, search, time } enum GenderTypeEnum { male, female } @@ -34,6 +34,70 @@ enum FamilyFileEnum { active, inactive, blocked, deleted, pending, rejected } enum BodyView { front, back } +enum HealthCalConEnum { calculator, converter } + +enum HealthCalculatorEnum { general, women } + +enum HealthCalculatorsTypeEnum { + bmi, + calories, + bmr, + idealBodyWeight, + bodyFat, + crabsProteinFat, + ovulation, + deliveryDueDate, + bloodSugar, + bloodCholesterol, + triglycerides +} + +extension HealthCalculatorExtenshion on HealthCalculatorsTypeEnum { + String get displayName { + AppState appState = getIt.get(); + bool isArabic = appState.getLanguageID() == 1 ? true : false; + switch (this) { + case HealthCalculatorsTypeEnum.bmi: + return isArabic ? "حاسبة مؤشر كتلة الجسم" : "BMI Calculator"; + case HealthCalculatorsTypeEnum.calories: + return isArabic ? "حاسبة السعرات الحرارية" : "Calories Calculator"; + case HealthCalculatorsTypeEnum.bmr: + return isArabic ? "حاسبة معدل الأيض الأساسي" : "BMR Calculator"; + case HealthCalculatorsTypeEnum.idealBodyWeight: + return isArabic ? "الوزن المثالي للجسم" : "Ideal Body Weight Calculator"; + case HealthCalculatorsTypeEnum.bodyFat: + return isArabic ? "حاسبة الدهون في الجسم" : "Body Fat Calculator"; + case HealthCalculatorsTypeEnum.crabsProteinFat: + return isArabic ? "حاسبة البروتين والدهون في سرطان البحر" : "Crabs Protein & Fat Calculator"; + case HealthCalculatorsTypeEnum.ovulation: + return isArabic ? "فترة الإباضة" : "Ovulation Period"; + case HealthCalculatorsTypeEnum.deliveryDueDate: + return isArabic ? "تاريخ استحقاق التسليم" : "Delivery Due Date"; + case HealthCalculatorsTypeEnum.bloodSugar: + return isArabic ? "سكر الدم" : "Blood Sugar"; + case HealthCalculatorsTypeEnum.bloodCholesterol: + return isArabic ? "كوليسترول الدم" : "Blood Cholesterol"; + case HealthCalculatorsTypeEnum.triglycerides: + return isArabic ? "الدهون الثلاثية في الدم" : "Triglycerides Fat Blood"; + } + } + + static LoginTypeEnum? fromValue(int value) { + switch (value) { + case 1: + return LoginTypeEnum.sms; + case 2: + return LoginTypeEnum.fingerprint; + case 3: + return LoginTypeEnum.face; + case 4: + return LoginTypeEnum.whatsapp; + default: + return null; + } + } +} + extension CalenderExtension on CalenderEnum { int get toInt { switch (this) { @@ -245,3 +309,5 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { // SymptomsChecker enum PossibleConditionsSeverityEnum { seekMedicalAdvice, monitorOnly, emergency } + +enum HealthTrackerTypeEnum { bloodSugar, bloodPressure, weightTracker } diff --git a/lib/core/exceptions/api_exception.dart b/lib/core/exceptions/api_exception.dart index eb11b71..eb0258f 100644 --- a/lib/core/exceptions/api_exception.dart +++ b/lib/core/exceptions/api_exception.dart @@ -1,7 +1,5 @@ import 'dart:convert'; -import 'package:equatable/equatable.dart'; -import 'package:hmg_patient_app_new/core/api/api_client.dart'; class APIException implements Exception { static const String BAD_REQUEST = 'api_common_bad_request'; diff --git a/lib/core/location_util.dart b/lib/core/location_util.dart index 487b228..9dcdbb5 100644 --- a/lib/core/location_util.dart +++ b/lib/core/location_util.dart @@ -12,8 +12,9 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:huawei_location/huawei_location.dart' as HmsLocation show FusedLocationProviderClient, Location, LocationSettingsRequest, LocationRequest; -import 'package:location/location.dart' show Location, PermissionStatus, LocationData; +import 'package:huawei_location/huawei_location.dart' as HmsLocation + show FusedLocationProviderClient, Location, LocationSettingsRequest, LocationRequest; +import 'package:location/location.dart' show Location; import 'package:permission_handler/permission_handler.dart' show Permission, PermissionListActions, PermissionStatusGetters, openAppSettings; class LocationUtils { @@ -59,37 +60,22 @@ class LocationUtils { // } void getLocation( - {Function(LatLng)? onSuccess, - VoidCallback? onFailure, - bool isShowConfirmDialog = false, - VoidCallback? onLocationDeniedForever}) async { + {Function(LatLng)? onSuccess, VoidCallback? onFailure, bool isShowConfirmDialog = false, VoidCallback? onLocationDeniedForever}) async { this.isShowConfirmDialog = isShowConfirmDialog; if (Platform.isIOS) { - getCurrentLocation( - onFailure: onFailure, - onSuccess: onSuccess, - onLocationDeniedForever: onLocationDeniedForever); + getCurrentLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever); return; } if (await isGMSDevice ?? true) { - getCurrentLocation( - onFailure: onFailure, - onSuccess: onSuccess, - onLocationDeniedForever: onLocationDeniedForever); + getCurrentLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever); return; } - getHMSLocation( - onFailure: onFailure, - onSuccess: onSuccess, - onLocationDeniedForever: onLocationDeniedForever); + getHMSLocation(onFailure: onFailure, onSuccess: onSuccess, onLocationDeniedForever: onLocationDeniedForever); } - void getCurrentLocation( - {Function(LatLng)? onSuccess, - VoidCallback? onFailure, - VoidCallback? onLocationDeniedForever}) async { + void getCurrentLocation({Function(LatLng)? onSuccess, VoidCallback? onFailure, VoidCallback? onLocationDeniedForever}) async { var location = Location(); bool isLocationEnabled = await location.serviceEnabled(); @@ -113,14 +99,12 @@ class LocationUtils { } } else if (permissionGranted == LocationPermission.deniedForever) { appState.resetLocation(); - if(onLocationDeniedForever == null && isShowConfirmDialog){ + if (onLocationDeniedForever == null && isShowConfirmDialog) { showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), navigationService.navigatorKey.currentContext!, child: Utils.getWarningWidget( - loadingText: - "Please grant location permission from app settings to see better results" - .needTranslation, + loadingText: "Please grant location permission from app settings to see better results".needTranslation, isShowActionButtons: true, onCancelTap: () { navigationService.pop(); @@ -253,10 +237,7 @@ class LocationUtils { appState.userLong = locationData.longitude; } - void getHMSLocation( - {VoidCallback? onFailure, - Function(LatLng p1)? onSuccess, - VoidCallback? onLocationDeniedForever}) async { + void getHMSLocation({VoidCallback? onFailure, Function(LatLng p1)? onSuccess, VoidCallback? onLocationDeniedForever}) async { try { var location = Location(); HmsLocation.FusedLocationProviderClient locationService = HmsLocation.FusedLocationProviderClient()..initFusedLocationService(); @@ -279,14 +260,12 @@ class LocationUtils { permissionGranted = await Geolocator.requestPermission(); if (permissionGranted == LocationPermission.deniedForever) { appState.resetLocation(); - if(onLocationDeniedForever == null && isShowConfirmDialog){ + if (onLocationDeniedForever == null && isShowConfirmDialog) { showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), navigationService.navigatorKey.currentContext!, child: Utils.getWarningWidget( - loadingText: - "Please grant location permission from app settings to see better results" - .needTranslation, + loadingText: "Please grant location permission from app settings to see better results".needTranslation, isShowActionButtons: true, onCancelTap: () { navigationService.pop(); @@ -311,7 +290,7 @@ class LocationUtils { HmsLocation.Location data = await locationService.getLastLocation(); if (data.latitude == null || data.longitude == null) { - appState.resetLocation(); + appState.resetLocation(); HmsLocation.LocationRequest request = HmsLocation.LocationRequest() ..priority = HmsLocation.LocationRequest.PRIORITY_HIGH_ACCURACY ..interval = 1000 // 1 second diff --git a/lib/core/post_params_model.dart b/lib/core/post_params_model.dart index cf52306..e13eb5c 100644 --- a/lib/core/post_params_model.dart +++ b/lib/core/post_params_model.dart @@ -14,19 +14,20 @@ class PostParamsModel { String? sessionID; String? setupID; - PostParamsModel( - {this.versionID, - this.channel, - this.languageID, - this.logInTokenID, - this.tokenID, - this.language, - this.ipAddress, - this.generalId, - this.latitude, - this.longitude, - this.deviceTypeID, - this.sessionID}); + PostParamsModel({ + this.versionID, + this.channel, + this.languageID, + this.logInTokenID, + this.tokenID, + this.language, + this.ipAddress, + this.generalId, + this.latitude, + this.longitude, + this.deviceTypeID, + this.sessionID, + }); PostParamsModel.fromJson(Map json) { versionID = json['VersionID']; diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index a42a44d..746d2a7 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -6,8 +6,6 @@ class DateUtil { /// convert String To Date function /// [date] String we want to convert static DateTime convertStringToDate(String? date) { - - if (date == null) return DateTime.now(); if (date.isEmpty) return DateTime.now(); @@ -522,6 +520,64 @@ class DateUtil { } return ""; } + + /// Get short month name from full month name + /// [monthName] Full month name like "January" + /// Returns short form like "Jan" + static String getShortMonthName(String monthName) { + switch (monthName.toLowerCase()) { + case 'january': + return 'Jan'; + case 'february': + return 'Feb'; + case 'march': + return 'Mar'; + case 'april': + return 'Apr'; + case 'may': + return 'May'; + case 'june': + return 'Jun'; + case 'july': + return 'Jul'; + case 'august': + return 'Aug'; + case 'september': + return 'Sep'; + case 'october': + return 'Oct'; + case 'november': + return 'Nov'; + case 'december': + return 'Dec'; + default: + return monthName; // Return as-is if not recognized + } + } + + /// Get short weekday name from full weekday name + /// [weekDayName] Full weekday name like "Monday" + /// Returns short form like "Mon" + static String getShortWeekDayName(String weekDayName) { + switch (weekDayName.toLowerCase().trim()) { + case 'monday': + return 'Mon'; + case 'tuesday': + return 'Tue'; + case 'wednesday': + return 'Wed'; + case 'thursday': + return 'Thu'; + case 'friday': + return 'Fri'; + case 'saturday': + return 'Sat'; + case 'sunday': + return 'Sun'; + default: + return weekDayName; // Return as-is if not recognized + } + } } extension OnlyDate on DateTime { diff --git a/lib/core/utils/doctor_response_mapper.dart b/lib/core/utils/doctor_response_mapper.dart index 994e9a1..05ed2fd 100644 --- a/lib/core/utils/doctor_response_mapper.dart +++ b/lib/core/utils/doctor_response_mapper.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:hmg_patient_app_new/core/cache_consts.dart' show CacheConst; -import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart' show RegionList, PatientDoctorAppointmentList, DoctorList, PatientDoctorAppointmentListByRegion; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; diff --git a/lib/core/utils/local_notifications.dart b/lib/core/utils/local_notifications.dart deleted file mode 100644 index aba01f8..0000000 --- a/lib/core/utils/local_notifications.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; - -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - -class LocalNotification { - Function(String payload)? _onNotificationClick; - static LocalNotification? _instance; - - static LocalNotification? getInstance() { - return _instance; - } - - static init({required Function(String payload) onNotificationClick}) { - if (_instance == null) { - _instance = LocalNotification(); - _instance?._onNotificationClick = onNotificationClick; - _instance?._initialize(); - } else { - // assert(false,(){ - // //TODO fix it - // "LocalNotification Already Initialized"; - // }); - } - } - - _initialize() async { - try { - var initializationSettingsAndroid = new AndroidInitializationSettings('app_icon'); - var initializationSettingsIOS = DarwinInitializationSettings(); - var initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS); - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) { - switch (notificationResponse.notificationResponseType) { - case NotificationResponseType.selectedNotification: - // selectNotificationStream.add(notificationResponse.payload); - break; - case NotificationResponseType.selectedNotificationAction: - // if (notificationResponse.actionId == navigationActionId) { - // selectNotificationStream.add(notificationResponse.payload); - // } - break; - } - }, - // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); - } catch (ex) { - print(ex.toString()); - } - // flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) - // { - // switch (notificationResponse.notificationResponseType) { - // case NotificationResponseType.selectedNotification: - // // selectNotificationStream.add(notificationResponse.payload); - // break; - // case NotificationResponseType.selectedNotificationAction: - // // if (notificationResponse.actionId == navigationActionId) { - // // selectNotificationStream.add(notificationResponse.payload); - // } - // // break; - // },} - // - // , - // - // ); - } - - // void notificationTapBackground(NotificationResponse notificationResponse) { - // // ignore: avoid_print - // print('notification(${notificationResponse.id}) action tapped: ' - // '${notificationResponse.actionId} with' - // ' payload: ${notificationResponse.payload}'); - // if (notificationResponse.input?.isNotEmpty ?? false) { - // // ignore: avoid_print - // print('notification action tapped with input: ${notificationResponse.input}'); - // } - // } - - var _random = new Random(); - - _randomNumber({int from = 100000}) { - return _random.nextInt(from); - } - - _vibrationPattern() { - var vibrationPattern = Int64List(4); - vibrationPattern[0] = 0; - vibrationPattern[1] = 1000; - vibrationPattern[2] = 5000; - vibrationPattern[3] = 2000; - - return vibrationPattern; - } - - Future? showNow({required String title, required String subtitle, required String payload}) { - Future.delayed(Duration(seconds: 1)).then((result) async { - var androidPlatformChannelSpecifics = AndroidNotificationDetails( - 'com.hmg.local_notification', - 'HMG', - channelDescription: 'HMG', - importance: Importance.max, - priority: Priority.high, - ticker: 'ticker', - vibrationPattern: _vibrationPattern(), - ongoing: true, - autoCancel: false, - usesChronometer: true, - when: DateTime.now().millisecondsSinceEpoch - 120 * 1000, - ); - var iOSPlatformChannelSpecifics = DarwinNotificationDetails(); - var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics); - await flutterLocalNotificationsPlugin.show(25613, title, subtitle, platformChannelSpecifics, payload: payload).catchError((err) { - print(err); - }); - }); - } - - Future scheduleNotification({required DateTime scheduledNotificationDateTime, required String title, required String description}) async { - ///vibrationPattern - var vibrationPattern = Int64List(4); - vibrationPattern[0] = 0; - vibrationPattern[1] = 1000; - vibrationPattern[2] = 5000; - vibrationPattern[3] = 2000; - - // var androidPlatformChannelSpecifics = AndroidNotificationDetails('active-prescriptions', 'ActivePrescriptions', - // channelDescription: 'ActivePrescriptionsDescription', - // // icon: 'secondary_icon', - // sound: RawResourceAndroidNotificationSound('slow_spring_board'), - // - // ///change it to be as ionic - // // largeIcon: DrawableResourceAndroidBitmap('sample_large_icon'),///change it to be as ionic - // vibrationPattern: vibrationPattern, - // enableLights: true, - // color: const Color.fromARGB(255, 255, 0, 0), - // ledColor: const Color.fromARGB(255, 255, 0, 0), - // ledOnMs: 1000, - // ledOffMs: 500); - // var iOSPlatformChannelSpecifics = DarwinNotificationDetails(sound: 'slow_spring_board.aiff'); - - // /change it to be as ionic - // var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics); - // await flutterLocalNotificationsPlugin.schedule(0, title, description, scheduledNotificationDateTime, platformChannelSpecifics); - } - - ///Repeat notification every day at approximately 10:00:00 am - Future showDailyAtTime() async { - // var time = Time(10, 0, 0); - // var androidPlatformChannelSpecifics = AndroidNotificationDetails('repeatDailyAtTime channel id', 'repeatDailyAtTime channel name', channelDescription: 'repeatDailyAtTime description'); - // var iOSPlatformChannelSpecifics = DarwinNotificationDetails(); - // var platformChannelSpecifics = NotificationDetails( - // androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); - // await flutterLocalNotificationsPlugin.showDailyAtTime( - // 0, - // 'show daily title', - // 'Daily notification shown at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}', - // time, - // platformChannelSpecifics); - } - - ///Repeat notification weekly on Monday at approximately 10:00:00 am - Future showWeeklyAtDayAndTime() async { - // var time = Time(10, 0, 0); - // var androidPlatformChannelSpecifics = AndroidNotificationDetails('show weekly channel id', 'show weekly channel name', channelDescription: 'show weekly description'); - // var iOSPlatformChannelSpecifics = DarwinNotificationDetails(); - // var platformChannelSpecifics = NotificationDetails( - // androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); - // await flutterLocalNotificationsPlugin.showWeeklyAtDayAndTime( - // 0, - // 'show weekly title', - // 'Weekly notification shown on Monday at approximately ${_toTwoDigitString(time.hour)}:${_toTwoDigitString(time.minute)}:${_toTwoDigitString(time.second)}', - // Day.Monday, - // time, - // platformChannelSpecifics); - } - - String _toTwoDigitString(int value) { - return value.toString().padLeft(2, '0'); - } - - Future cancelNotification() async { - await flutterLocalNotificationsPlugin.cancel(0); - } - - Future cancelAllNotifications() async { - await flutterLocalNotificationsPlugin.cancelAll(); - } -} diff --git a/lib/core/utils/penguin_method_channel.dart b/lib/core/utils/penguin_method_channel.dart new file mode 100644 index 0000000..1f19037 --- /dev/null +++ b/lib/core/utils/penguin_method_channel.dart @@ -0,0 +1,105 @@ +import 'package:flutter/services.dart'; + +class PenguinMethodChannel { + static const MethodChannel _channel = MethodChannel('launch_penguin_ui'); + + Future loadGif() async { + return await rootBundle.load("assets/images/progress-loading-red-crop-1.gif").then((data) => data.buffer.asUint8List()); + } + + Future launch(String storyboardName, String languageCode, String username, {NavigationClinicDetails? details}) async { + // Uint8List image = await loadGif(); + try { + await _channel.invokeMethod('launchPenguin', { + "storyboardName": storyboardName, + "baseURL": "https://penguinuat.hmg.com", + // "dataURL": "https://hmg.nav.penguinin.com", + // "positionURL": "https://hmg.nav.penguinin.com", + // "dataURL": "https://hmg-v33.local.penguinin.com", + // "positionURL": "https://hmg-v33.local.penguinin.com", + "dataURL": "https://penguinuat.hmg.com", + "positionURL": "https://penguinuat.hmg.com", + "dataServiceName": "api", + "positionServiceName": "pe", + "clientID": "HMG", + "clientKey": "UGVuZ3VpbklOX1Blbk5hdl9QSUY=", + "username": details?.patientId ?? "Haroon", + // "username": "Haroon", + "isSimulationModeEnabled": false, + "isShowUserName": false, + "isUpdateUserLocationSmoothly": true, + "isEnableReportIssue": true, + "languageCode": languageCode, + "mapBoxKey": "pk.eyJ1IjoicndhaWQiLCJhIjoiY2x5cGo4aHNjMGNsbTJyc2djempobGQxaSJ9.RCaC6WrUt4A4YnZNfxnONQ", + "clinicID": details?.clinicId ?? "", + // "clinicID": "108", // 46 ,49, 133 + "patientID": details?.patientId ?? "", + "projectID": int.parse(details?.projectId ?? "-1"), + // "loaderImage": image, + }); + } on PlatformException catch (e) { + print("Failed to launch PenguinIn: '${e.message}'."); + } + } + + void setMethodCallHandler(){ + _channel.setMethodCallHandler((MethodCall call) async { + try { + + print(call.method); + + switch (call.method) { + + case PenguinMethodNames.onPenNavInitializationError: + _handleInitializationError(call.arguments); // Handle onPenNavInitializationError errors. + break; + case PenguinMethodNames.onPenNavUIDismiss: + //todo handle pen dismissable + // _handlePenNavUIDismiss(); // Handle UI dismissal event. + break; + case PenguinMethodNames.onReportIssue: + // Handle the report issue event. + _handleInitializationError(call.arguments); + break; + default: + _handleUnknownMethod(call.method); // Handle unknown method calls. + } + } catch (e) { + print("Error handling method call '${call.method}': $e"); + // Optionally, log this error to an external service + } + }); + } + static void _handleUnknownMethod(String method) { + print("Unknown method: $method"); + // Optionally, handle this unknown method case, such as reporting or ignoring it + } + + + static void _handleInitializationError(Map error) { + final type = error['type'] as String?; + final description = error['description'] as String?; + print("Initialization Error: ${type ?? 'Unknown Type'}, ${description ?? 'No Description'}"); + + } + +} +// Define constants for method names +class PenguinMethodNames { + static const String showPenguinUI = 'showPenguinUI'; + static const String openSharedLocation = 'openSharedLocation'; + + // ---- Handler Method + static const String onPenNavSuccess = 'onPenNavSuccess'; // Tested Android,iOS + static const String onPenNavInitializationError = 'onPenNavInitializationError'; // Tested Android,iOS + static const String onPenNavUIDismiss = 'onPenNavUIDismiss'; //Tested Android,iOS + static const String onReportIssue = 'onReportIssue'; // Tested Android,iOS + static const String onLocationOffCampus = 'onLocationOffCampus'; // Tested iOS,Android + static const String navigateToPOI = 'navigateToPOI'; // Tested Android,iOS +} + +class NavigationClinicDetails { + String? clinicId; + String? patientId; + String? projectId; +} diff --git a/lib/core/utils/push_notification_handler.dart b/lib/core/utils/push_notification_handler.dart index ee05335..88e8cc8 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -15,18 +15,11 @@ import 'package:flutter_callkit_incoming/entities/notification_params.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_ios_voip_kit_karmm/call_state_type.dart'; import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart'; -// import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:get_it/get_it.dart'; -import 'package:hmg_patient_app_new/core/utils/local_notifications.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:uuid/uuid.dart'; -import '../cache_consts.dart'; - // |--> Push Notification Background @pragma('vm:entry-point') Future backgroundMessageHandler(dynamic message) async { @@ -38,7 +31,7 @@ Future backgroundMessageHandler(dynamic message) async { // showCallkitIncoming(message); _incomingCall(message.data); return; - } else {} + } } callPage(String sessionID, String token) async {} @@ -325,7 +318,7 @@ class PushNotificationHandler { if (fcmToken != null) onToken(fcmToken); // } } catch (ex) { - print("Notification Exception: " + ex.toString()); + print("Notification Exception: $ex"); } FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); } @@ -333,7 +326,7 @@ class PushNotificationHandler { if (Platform.isIOS) { final permission = await FirebaseMessaging.instance.requestPermission(); await FirebaseMessaging.instance.getAPNSToken().then((value) async { - log("APNS token: " + value.toString()); + log("APNS token: $value"); await Utils.saveStringFromPrefs(CacheConst.apnsToken, value.toString()); }); await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( @@ -380,14 +373,14 @@ class PushNotificationHandler { }); FirebaseMessaging.instance.getToken().then((String? token) { - print("Push Notification getToken: " + token!); + print("Push Notification getToken: ${token!}"); onToken(token!); }).catchError((err) { print(err); }); FirebaseMessaging.instance.onTokenRefresh.listen((fcm_token) { - print("Push Notification onTokenRefresh: " + fcm_token); + print("Push Notification onTokenRefresh: $fcm_token"); onToken(fcm_token); }); @@ -403,7 +396,7 @@ class PushNotificationHandler { } newMessage(RemoteMessage remoteMessage) async { - print("Remote Message: " + remoteMessage.data.toString()); + print("Remote Message: ${remoteMessage.data}"); if (remoteMessage.data.isEmpty) { return; } @@ -429,7 +422,7 @@ class PushNotificationHandler { } onToken(String token) async { - print("Push Notification Token: " + token); + print("Push Notification Token: $token"); await Utils.saveStringFromPrefs(CacheConst.pushToken, token); } @@ -443,9 +436,7 @@ class PushNotificationHandler { Future requestPermissions() async { try { if (Platform.isIOS) { - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() - ?.requestPermissions(alert: true, badge: true, sound: true); + await FirebaseMessaging.instance.requestPermission(alert: true, badge: true, sound: true); } else if (Platform.isAndroid) { Map statuses = await [ Permission.notification, diff --git a/lib/core/utils/size_config.dart b/lib/core/utils/size_config.dart index 9f9d835..0f8766a 100644 --- a/lib/core/utils/size_config.dart +++ b/lib/core/utils/size_config.dart @@ -1,6 +1,5 @@ import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; -import 'package:hmg_patient_app_new/core/cache_consts.dart'; class SizeConfig { static double _blockWidth = 0; diff --git a/lib/core/utils/size_utils.dart b/lib/core/utils/size_utils.dart index fdd0d30..02b8195 100644 --- a/lib/core/utils/size_utils.dart +++ b/lib/core/utils/size_utils.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:math' as math; import 'package:flutter/material.dart'; // These are the Viewport values of your Figma Design. @@ -6,6 +7,14 @@ import 'package:flutter/material.dart'; // These are the Viewport values of your const num figmaDesignWidth = 375; // iPhone X / 12 base width const num figmaDesignHeight = 812; // iPhone X / 12 base height +extension ConstrainedResponsive on num { + /// Width with max cap for tablets + double get wCapped => isTablet ? math.min(w, this * 1.3) : w; + + /// Height with max cap for tablets + double get hCapped => isTablet ? math.min(h, this * 1.3) : h; +} + extension ResponsiveExtension on num { double get _screenWidth => SizeUtils.width; @@ -27,7 +36,7 @@ extension ResponsiveExtension on num { double clamp; if (SizeUtils.deviceType == DeviceType.tablet || _isFoldable) { // More conservative scaling for tablets and foldables - clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.4 : 1.1; + clamp = (aspectRatio > 1.5 || aspectRatio < 0.67) ? 1.6 : 1.4; } else { // Original logic for phones clamp = (aspectRatio > 1.3 || aspectRatio < 0.77) ? 1.6 : 1.2; @@ -68,7 +77,7 @@ extension ResponsiveExtension on num { double get r { double baseScale = (this * _screenWidth) / figmaDesignWidth; - if (_isFoldable) { + if (_isFoldable || isTablet) { // Use the same logic as enhanced width for foldables double scale = _screenWidth / figmaDesignWidth; scale = scale.clamp(0.8, 1.4); diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 857c0c2..f5ffd36 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -39,6 +39,50 @@ class Utils { static bool get isLoading => _isLoadingVisible; + static var navigationProjectsList = [ + { + "Desciption": "Sahafa Hospital", + "DesciptionN": "مستشفى الصحافة", + "ID": 1, + "LegalName": "Sahafa Hospital", + "LegalNameN": "مستشفى الصحافة", + "Name": "Sahafa Hospital", + "NameN": "مستشفى الصحافة", + "PhoneNumber": "+966115222222", + "SetupID": "013311", + "DistanceInKilometers": 0, + "HasVida3": false, + "IsActive": true, + "IsHmg": true, + "IsVidaPlus": false, + "Latitude": "24.8113774", + "Longitude": "46.6239813", + "MainProjectID": 130, + "ProjectOutSA": false, + "UsingInDoctorApp": false + },{ + "Desciption": "Jeddah Hospital", + "DesciptionN": "مستشفى الصحافة", + "ID": 3, + "LegalName": "Jeddah Hospital", + "LegalNameN": "مستشفى الصحافة", + "Name": "Jeddah Hospital", + "NameN": "مستشفى الصحافة", + "PhoneNumber": "+966115222222", + "SetupID": "013311", + "DistanceInKilometers": 0, + "HasVida3": false, + "IsActive": true, + "IsHmg": true, + "IsVidaPlus": false, + "Latitude": "24.8113774", + "Longitude": "46.6239813", + "MainProjectID": 130, + "ProjectOutSA": false, + "UsingInDoctorApp": false + } + ]; + static void showToast(String message, {bool longDuration = true}) { Fluttertoast.showToast( msg: message, @@ -218,16 +262,6 @@ class Utils { return await prefs.remove(key); } - static void showLoading({bool isNeedBinding = true}) { - if (isNeedBinding) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showLoadingDialog(); - }); - } else { - showLoadingDialog(); - } - } - static void showLoadingDialog() { _isLoadingVisible = true; showDialog( @@ -244,18 +278,6 @@ class Utils { ); } - static void hideLoading() { - try { - if (_isLoadingVisible) { - _isLoadingVisible = false; - Navigator.of(navigationService.navigatorKey.currentContext!).pop(); - } - _isLoadingVisible = false; - } catch (e) { - log("errr: ${e.toString()}"); - } - } - static List uniqueBy(List list, K Function(T) keySelector) { final seenKeys = {}; return list.where((item) => seenKeys.add(keySelector(item))).toList(); @@ -326,7 +348,7 @@ class Utils { children: [ SizedBox(height: isSmallWidget ? 0.h : 48.h), Lottie.asset(AppAnimations.noData, - repeat: false, reverse: false, frameRate: FrameRate(60), width: width.h, height: height.h, fit: BoxFit.fill), + repeat: false, reverse: false, frameRate: FrameRate(60), width: width.w, height: height.h, fit: BoxFit.fill), SizedBox(height: 16.h), (noDataText ?? LocaleKeys.noDataAvailable.tr()) .toText14(weight: FontWeight.w500, color: AppColors.greyTextColor, isCenter: true) @@ -351,10 +373,10 @@ class Utils { ).center; } - static Widget getSuccessWidget({String? loadingText}) { + static Widget getSuccessWidget({String? loadingText, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) { return Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: crossAxisAlignment, children: [ Lottie.asset(AppAnimations.checkmark, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), @@ -722,7 +744,16 @@ class Utils { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Image.asset(AppAssets.mada, width: 25.h, height: 25.h), - Image.asset(AppAssets.tamaraEng, width: 25.h, height: 25.h), + Image.asset( + AppAssets.tamaraEng, + width: 25.h, + height: 25.h, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + debugPrint('Failed to load Tamara PNG in payment methods: $error'); + return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 25.h, height: 25.h, fit: BoxFit.contain); + }, + ), Image.asset(AppAssets.visa, width: 25.h, height: 25.h), Image.asset(AppAssets.mastercard, width: 25.h, height: 25.h), Image.asset(AppAssets.applePay, width: 25.h, height: 25.h), @@ -859,6 +890,17 @@ class Utils { isHMC: hospital.isHMC); } + + static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) { + if (item == null) return null; + return HospitalsModel( + name: item.filterName, + nameN: item.filterName, + distanceInKilometers: item.distanceInKMs, + isHMC: item.isHMC, + ); + } + static bool havePrivilege(int id) { bool isHavePrivilege = false; try { @@ -876,7 +918,6 @@ class Utils { launchUrl(uri, mode: LaunchMode.inAppBrowserView); } - static Color getCardBorderColor(int currentQueueStatus) { switch (currentQueueStatus) { case 0: @@ -913,16 +954,23 @@ class Utils { return AppColors.primaryRedColor; } - static String getCardButtonText(int currentQueueStatus) { + static String getCardButtonText(int currentQueueStatus, String roomNumber) { switch (currentQueueStatus) { case 0: return "Please wait! you will be called for vital signs".needTranslation; case 1: - return "Please visit Room S5 for vital signs".needTranslation; + return "Please visit Room $roomNumber for vital signs".needTranslation; case 2: - return "Please visit Room S5 to the Doctor".needTranslation; + return "Please visit Room $roomNumber to the Doctor".needTranslation; } return ""; } + static bool isDateToday(DateTime dateToCheck) { + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final DateTime checkDate = DateTime(dateToCheck.year, dateToCheck.month, dateToCheck.day); + + return checkDate == today; + } } diff --git a/lib/extensions/int_extensions.dart b/lib/extensions/int_extensions.dart index 80b3171..75460c5 100644 --- a/lib/extensions/int_extensions.dart +++ b/lib/extensions/int_extensions.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; extension IntExtensions on int { Widget get height => SizedBox(height: toDouble()); diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 2039fb8..309dde1 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -23,14 +23,15 @@ extension CapExtension on String { extension EmailValidator on String { Widget get toWidget => Text(this); - Widget toText8({Color? color, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text( + Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => + Text( this, maxLines: maxlines, overflow: textOverflow, style: TextStyle( fontSize: 8.f, fontStyle: fontStyle ?? FontStyle.normal, - fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: 0, ), @@ -41,7 +42,7 @@ extension EmailValidator on String { FontWeight? weight, bool isBold = false, bool isUnderLine = false, - bool isCenter = false, + bool isCenter = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, @@ -191,7 +192,8 @@ extension EmailValidator on String { letterSpacing: letterSpacing, height: height, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), - decoration: isUnderLine ? TextDecoration.underline : null), + decoration: isUnderLine ? TextDecoration.underline : null, + decorationColor: color ?? AppColors.blackColor), ); Widget toText15( @@ -214,39 +216,38 @@ extension EmailValidator on String { decoration: isUnderLine ? TextDecoration.underline : null), ); - Widget toText16({ - Color? color, - bool isUnderLine = false, - bool isBold = false, - bool isCenter = false, - int? maxlines, - double? height, - TextAlign? textAlign, - FontWeight? weight, - TextOverflow? textOverflow, - double? letterSpacing = -0.4, - Color decorationColor =AppColors.errorColor - }) => + Widget toText16( + {Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + int? maxlines, + double? height, + TextAlign? textAlign, + FontWeight? weight, + TextOverflow? textOverflow, + double? letterSpacing = -0.4, + Color decorationColor = AppColors.errorColor}) => Text( this, maxLines: maxlines, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - color: color ?? AppColors.blackColor, - fontSize: 16.f, - letterSpacing: letterSpacing, - height: height, - overflow: textOverflow, - fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), - decoration: isUnderLine ? TextDecoration.underline : null, - decorationColor: decorationColor - ), + color: color ?? AppColors.blackColor, + fontSize: 16.f, + letterSpacing: letterSpacing, + height: height, + overflow: textOverflow, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + decoration: isUnderLine ? TextDecoration.underline : null, + decorationColor: decorationColor), ); Widget toText17({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle( + color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow}) => Text( @@ -255,39 +256,62 @@ extension EmailValidator on String { this, overflow: textOverflow, style: TextStyle( - fontSize: 18.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4), + fontSize: 18.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4), ); Widget toText19({Color? color, bool isBold = false}) => Text( this, - style: TextStyle(fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), + style: TextStyle( + fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), ); - Widget toText20({Color? color, FontWeight? weight, bool isBold = false, }) => Text( + Widget toText20({ + Color? color, + FontWeight? weight, + bool isBold = false, + }) => + Text( this, style: TextStyle( - fontSize: 20.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4), + fontSize: 20.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4), ); Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( this, maxLines: maxlines, style: TextStyle( - color: color ?? AppColors.blackColor, fontSize: 21.f, letterSpacing: -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + color: color ?? AppColors.blackColor, + fontSize: 21.f, + letterSpacing: -1, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); Widget toText22({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - height: 1, color: color ?? AppColors.blackColor, fontSize: 22.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + height: 1, + color: color ?? AppColors.blackColor, + fontSize: 22.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing??-1, fontWeight: isBold ? FontWeight.bold : fontWeight??FontWeight.normal), + height: 23 / 24, + color: color ?? AppColors.blackColor, + fontSize: 24.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), ); Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing}) => Text( @@ -312,17 +336,25 @@ extension EmailValidator on String { fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); - Widget toText32({Color? color, bool isBold = false, bool isCenter = false}) => Text( + Widget toText32({FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + height: 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 32.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal), ); Widget toText44({Color? color, bool isBold = false}) => Text( this, style: TextStyle( - height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + height: 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 44.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) { diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index c9796e8..9b09c43 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -5,10 +5,8 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; -import 'package:hmg_patient_app_new/core/common_models/privilege/PrivilegeModel.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; -import 'package:hmg_patient_app_new/features/authentication/models/request_models/check_activation_code_register_request_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -262,10 +260,10 @@ class AuthenticationRepoImp implements AuthenticationRepo { newRequest.forRegisteration = newRequest.isRegister ?? false; newRequest.isRegister = false; //silent login case removed token and login token - // if(newRequest.logInTokenID.isEmpty && newRequest.isSilentLogin == true) { - // newRequest.logInTokenID = null; - // newRequest.deviceToken = null; - // } + if(newRequest.logInTokenID.isEmpty && newRequest.isSilentLogin == true && (newRequest.loginType==1 || newRequest.loginType==4)) { + newRequest.logInTokenID = null; + newRequest.deviceToken = null; + } } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index fa16423..b393772 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -27,12 +27,12 @@ import 'package:hmg_patient_app_new/features/authentication/models/resp_models/a import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_activation_code_resp_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; -import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -40,6 +40,7 @@ import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; import 'models/request_models/get_user_mobile_device_data.dart'; @@ -566,7 +567,7 @@ class AuthenticationViewModel extends ChangeNotifier { if (!_appState.getIsChildLoggedIn) { await medicalVm.getFamilyFiles(status: 0); await medicalVm.getAllPendingRecordsByResponseId(); - _navigationService.popUntilNamed(AppRoutes.landingScreen); + _navigationService.replaceAllRoutesAndNavigateToLanding(); } } else { if (activation.list != null && activation.list!.isNotEmpty) { @@ -586,6 +587,7 @@ class AuthenticationViewModel extends ChangeNotifier { activation.list!.first.bloodGroup = activation.patientBlodType; _appState.setAuthenticatedUser(activation.list!.first); _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); + _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; } // _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); _appState.setAppAuthToken = activation.authenticationTokenId; @@ -675,7 +677,12 @@ class AuthenticationViewModel extends ChangeNotifier { } Future navigateToHomeScreen() async { - _navigationService.pushAndReplace(AppRoutes.landingScreen); + Navigator.pushAndRemoveUntil( + _navigationService.navigatorKey.currentContext!, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); } Future navigateToOTPScreen( diff --git a/lib/features/blood_donation/blood_donation_repo.dart b/lib/features/blood_donation/blood_donation_repo.dart index 84997b2..5643635 100644 --- a/lib/features/blood_donation/blood_donation_repo.dart +++ b/lib/features/blood_donation/blood_donation_repo.dart @@ -3,14 +3,26 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_hospitals_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_response_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/models/cities_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class BloodDonationRepo { Future>>> getAllCities(); + Future>>> getProjectList(); + + Future>>> getBloodDonationProjectsList(); + Future>> getPatientBloodGroupDetails(); + + Future>> updateBloodGroup({required Map request}); + + Future>> getFreeBloodDonationSlots({required Map request}); + + Future>> addUserAgreementForBloodDonation({required Map request}); } class BloodDonationRepoImp implements BloodDonationRepo { @@ -66,6 +78,7 @@ class BloodDonationRepoImp implements BloodDonationRepo { await apiClient.post( GET_BLOOD_REQUEST, body: mapDevice, + isAllowAny: true, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; }, @@ -92,4 +105,186 @@ class BloodDonationRepoImp implements BloodDonationRepo { return Left(UnknownFailure(e.toString())); } } -} \ No newline at end of file + + @override + Future>>> getProjectList() async { + Map request = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_PROJECT_LIST, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['ListProject']; + + final appointmentsList = list.map((item) => HospitalsModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getBloodDonationProjectsList() async { + Map request = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getProjectsHaveBDClinics, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final listData = (response['BD_getProjectsHaveBDClinics'] as List); + final list = listData.map((item) => BdGetProjectsHaveBdClinic.fromJson(item as Map)).toList(); + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> updateBloodGroup({required Map request}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.bloodGroupUpdate, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // final list = response['ListProject']; + + // final appointmentsList = list.map((item) => HospitalsModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getFreeBloodDonationSlots({required Map request}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getClinicsBDFreeSlots, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // final list = response['ListProject']; + + // final appointmentsList = list.map((item) => HospitalsModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> addUserAgreementForBloodDonation({required Map request}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.userAgreementForBloodGroupUpdate, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // final list = response['ListProject']; + + // final appointmentsList = list.map((item) => HospitalsModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/blood_donation/blood_donation_view_model.dart b/lib/features/blood_donation/blood_donation_view_model.dart index f345359..8325cf7 100644 --- a/lib/features/blood_donation/blood_donation_view_model.dart +++ b/lib/features/blood_donation/blood_donation_view_model.dart @@ -1,14 +1,25 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/doctor_response_mapper.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_repo.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_hospitals_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_list_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_response_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/models/cities_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; class BloodDonationViewModel extends ChangeNotifier { final DialogService dialogService; @@ -16,8 +27,21 @@ class BloodDonationViewModel extends ChangeNotifier { ErrorHandlerService errorHandlerService; final NavigationService navigationService; final AppState appState; + bool isTermsAccepted = false; + BdGetProjectsHaveBdClinic? selectedHospital; + CitiesModel? selectedCity; + BloodGroupListModel? selectedBloodGroup; + int _selectedHospitalIndex = 0; + int _selectedBloodTypeIndex = 0; + GenderTypeEnum? selectedGender; + String? selectedBloodType; + final NavigationService navServices; + + List hospitalList = []; List citiesList = []; + List_BloodGroupDetailsModel patientBloodGroupDetailsModel = List_BloodGroupDetailsModel(); + List bloodGroupList = [ BloodGroupListModel("O+", 0), BloodGroupListModel("O-", 1), @@ -29,35 +53,34 @@ class BloodDonationViewModel extends ChangeNotifier { BloodGroupListModel("B-", 7), ]; - List genderList = [ - BloodGroupListModel(LocaleKeys.malE.tr(), 1), - BloodGroupListModel(LocaleKeys.female.tr(), 2), - ]; - - late CitiesModel selectedCity; - late BloodGroupListModel selectedBloodGroup; - int _selectedHospitalIndex = 0; - int _selectedBloodTypeIndex = 0; - String selectedBloodType = ''; - - List_BloodGroupDetailsModel patientBloodGroupDetailsModel = List_BloodGroupDetailsModel(); - - BloodDonationViewModel({required this.bloodDonationRepo, required this.errorHandlerService, required this.navigationService, required this.dialogService, required this.appState}); + BloodDonationViewModel({ + required this.bloodDonationRepo, + required this.errorHandlerService, + required this.navigationService, + required this.dialogService, + required this.appState, + required this.navServices, + }); setSelectedCity(CitiesModel city) { selectedCity = city; notifyListeners(); } + void onGenderChange(String? status) { + selectedGender = GenderTypeExtension.fromType(status)!; + notifyListeners(); + } + setSelectedBloodGroup(BloodGroupListModel bloodGroup) { selectedBloodGroup = bloodGroup; - selectedBloodType = selectedBloodGroup.name; + selectedBloodType = selectedBloodGroup!.name; notifyListeners(); } Future getRegionSelectedClinics({Function(dynamic)? onSuccess, Function(String)? onError}) async { citiesList.clear(); - selectedCity = CitiesModel(); + selectedCity = null; notifyListeners(); final result = await bloodDonationRepo.getAllCities(); @@ -70,6 +93,7 @@ class BloodDonationViewModel extends ChangeNotifier { onError!(apiResponse.errorMessage ?? 'An unexpected error occurred'); } else if (apiResponse.messageStatus == 1) { citiesList = apiResponse.data!; + citiesList.sort((a, b) => a.description!.compareTo(b.description!)); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -99,7 +123,7 @@ class BloodDonationViewModel extends ChangeNotifier { citiesModel.descriptionN = citiesList[_selectedHospitalIndex].descriptionN; selectedCity = citiesModel; selectedBloodType = patientBloodGroupDetailsModel.bloodGroup!; - _selectedBloodTypeIndex = getBloodIndex(selectedBloodType); + _selectedBloodTypeIndex = getBloodIndex(selectedBloodType ?? ''); notifyListeners(); if (onSuccess != null) { @@ -112,11 +136,11 @@ class BloodDonationViewModel extends ChangeNotifier { int getSelectedCityID() { int cityID = 1; - citiesList.forEach((element) { + for (var element in citiesList) { if (element.description == patientBloodGroupDetailsModel.city) { cityID = element.iD!; } - }); + } return cityID; } @@ -143,4 +167,120 @@ class BloodDonationViewModel extends ChangeNotifier { return 0; } } + + void onTermAccepted() { + isTermsAccepted = !isTermsAccepted; + notifyListeners(); + } + + bool isUserAuthanticated() { + print("the app state is ${appState.isAuthenticated}"); + if (!appState.isAuthenticated) { + return false; + } else { + return true; + } + } + + Future fetchHospitalsList() async { + // hospitalList.clear(); + notifyListeners(); + final result = await bloodDonationRepo.getBloodDonationProjectsList(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) async { + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + hospitalList = apiResponse.data!; + hospitalList.sort((a, b) => a.projectName!.compareTo(b.projectName!)); + notifyListeners(); + } + }, + ); + } + + Future getFreeBloodDonationSlots({required Map request}) async { + final result = await bloodDonationRepo.getFreeBloodDonationSlots(request: request); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) async { + if (apiResponse.messageStatus == 2) { + } else if (apiResponse.messageStatus == 1) { + // TODO: Handle free slots data + print(apiResponse.data['BD_FreeSlots']); + notifyListeners(); + } + }, + ); + } + + bool isLocationEnabled() { + return appState.userLong != 0.0 && appState.userLong != 0.0; + } + + setSelectedHospital(BdGetProjectsHaveBdClinic hospital) { + selectedHospital = hospital; + notifyListeners(); + } + + Future validateSelections() async { + if (selectedCity == null) { + await dialogService.showErrorBottomSheet( + message: "Please choose city", + ); + return false; + } + + if (selectedBloodGroup == null) { + await dialogService.showErrorBottomSheet( + message: "Please choose Gender", + ); + return false; + } + + if (selectedBloodType == null) { + await dialogService.showErrorBottomSheet( + message: "Please choose Blood Group", + ); + return false; + } + + if (!isTermsAccepted) { + await dialogService.showErrorBottomSheet( + message: "Please accept Terms and Conditions to continue", + ); + return false; + } + return true; + } + + Future updateBloodGroup() async { + LoaderBottomSheet.showLoader(); + // body['City'] = detailsModel.city; + // body['cityCode'] = detailsModel.cityCode; + // body['Gender'] = detailsModel.gender; + // body['BloodGroup'] = detailsModel.bloodGroup; + // body['CellNumber'] = user.mobileNumber; + // body['LanguageID'] = languageID; + // body['NationalID'] = user.nationalityID; + // body['ZipCode'] = user.zipCode ?? "+966"; + // body['isDentalAllowedBackend'] = false; + Map payload = { + "City": selectedCity?.description, + "cityCode": selectedCity?.iD, + "Gender": selectedGender?.value, + "isDentalAllowedBackend": false + // "Gender": selectedGender?.value, + }; + await bloodDonationRepo.updateBloodGroup(request: payload); + await addUserAgreementForBloodDonation(); + LoaderBottomSheet.hideLoader(); + } + + Future addUserAgreementForBloodDonation() async { + Map payload = {"IsAgreed": true}; + await bloodDonationRepo.addUserAgreementForBloodDonation(request: payload); + } } diff --git a/lib/features/blood_donation/models/blood_group_hospitals_model.dart b/lib/features/blood_donation/models/blood_group_hospitals_model.dart new file mode 100644 index 0000000..10b4e67 --- /dev/null +++ b/lib/features/blood_donation/models/blood_group_hospitals_model.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; + +class BdProjectsHaveBdClinicsModel { + List? bdGetProjectsHaveBdClinics; + + BdProjectsHaveBdClinicsModel({ + this.bdGetProjectsHaveBdClinics, + }); + + factory BdProjectsHaveBdClinicsModel.fromRawJson(String str) => BdProjectsHaveBdClinicsModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory BdProjectsHaveBdClinicsModel.fromJson(Map json) => BdProjectsHaveBdClinicsModel( + bdGetProjectsHaveBdClinics: json["BD_getProjectsHaveBDClinics"] == null ? [] : List.from(json["BD_getProjectsHaveBDClinics"]!.map((x) => BdGetProjectsHaveBdClinic.fromJson(x))), + ); + + Map toJson() => { + "BD_getProjectsHaveBDClinics": bdGetProjectsHaveBdClinics == null ? [] : List.from(bdGetProjectsHaveBdClinics!.map((x) => x.toJson())), + }; +} + +class BdGetProjectsHaveBdClinic { + int? rowId; + int? id; + int? projectId; + int? numberOfRooms; + bool? isActive; + int? createdBy; + String? createdOn; + dynamic editedBy; + dynamic editedOn; + String? projectName; + dynamic projectNameN; + + BdGetProjectsHaveBdClinic({ + this.rowId, + this.id, + this.projectId, + this.numberOfRooms, + this.isActive, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.projectName, + this.projectNameN, + }); + + factory BdGetProjectsHaveBdClinic.fromRawJson(String str) => BdGetProjectsHaveBdClinic.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory BdGetProjectsHaveBdClinic.fromJson(Map json) => BdGetProjectsHaveBdClinic( + rowId: json["RowID"], + id: json["ID"], + projectId: json["ProjectID"], + numberOfRooms: json["NumberOfRooms"], + isActive: json["IsActive"], + createdBy: json["CreatedBy"], + createdOn: json["CreatedOn"], + editedBy: json["EditedBy"], + editedOn: json["EditedON"], + projectName: json["ProjectName"], + projectNameN: json["ProjectNameN"], + ); + + Map toJson() => { + "RowID": rowId, + "ID": id, + "ProjectID": projectId, + "NumberOfRooms": numberOfRooms, + "IsActive": isActive, + "CreatedBy": createdBy, + "CreatedOn": createdOn, + "EditedBy": editedBy, + "EditedON": editedOn, + "ProjectName": projectName, + "ProjectNameN": projectNameN, + }; +} diff --git a/lib/features/blood_donation/widgets/hospital_selection.dart b/lib/features/blood_donation/widgets/hospital_selection.dart new file mode 100644 index 0000000..288ac34 --- /dev/null +++ b/lib/features/blood_donation/widgets/hospital_selection.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_hospitals_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; +import 'package:provider/provider.dart'; + +class HospitalBottomSheetBodySelection extends StatelessWidget { + final Function(BdGetProjectsHaveBdClinic userSelection) onUserHospitalSelection; + + const HospitalBottomSheetBodySelection({super.key, required this.onUserHospitalSelection(BdGetProjectsHaveBdClinic userSelection)}); + + @override + Widget build(BuildContext context) { + final bloodDonationVm = Provider.of(context, listen: false); + AppState appState = getIt.get(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Please select the hospital you want to make an appointment.".needTranslation, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ), + SizedBox(height: 16.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + hospitalName(bloodDonationVm.hospitalList[index]).onPress(() { + onUserHospitalSelection(bloodDonationVm.hospitalList[index]); + Navigator.of(context).pop(); + }) + ], + ), + ), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.blackColor, width: 40.h, height: 40.h, fit: BoxFit.contain), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).onPress(() { + bloodDonationVm.setSelectedHospital(bloodDonationVm.hospitalList[index]); + Navigator.of(context).pop(); + }); + }, + separatorBuilder: (_, __) => SizedBox(height: 16.h), + itemCount: bloodDonationVm.hospitalList.length), + ) + ], + ); + } + + Widget hospitalName(dynamic hospital) => Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.hmg).paddingOnly(right: 10), + Expanded( + child: Text(hospital.projectName ?? "", style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: AppColors.blackColor)), + ) + ], + ); +} diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 3683d57..cfd473e 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; @@ -6,6 +5,7 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; @@ -26,9 +26,12 @@ abstract class BookAppointmentsRepo { Future>>> getDoctorsList(int clinicID, int projectID, bool isNearest, int doctorId, String doctorName, {isContinueDentalPlan = false}); + Future>>> getDoctorsListByHealthCal(int calculationID); + Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}); - Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}); + Future>> getDoctorFreeSlots(int clinicID, int projectID, int doctorId, bool isBookingForLiveCare, + {bool continueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}); Future>> cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}); @@ -83,8 +86,7 @@ abstract class BookAppointmentsRepo { Future>>> getDentalChiefComplaintDoctorsList(int projectID, int chiefComplaintID, {Function(dynamic)? onSuccess, Function(String)? onError}); - Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, - {Function(dynamic)? onSuccess, Function(String)? onError}); + Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, {Function(dynamic)? onSuccess, Function(String)? onError}); Future>> checkScannedNFCAndQRCode(String nfcCode, int projectId, {Function(dynamic)? onSuccess, Function(String)? onError}); @@ -101,6 +103,8 @@ abstract class BookAppointmentsRepo { required int userAge, Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> getAppointmentNearestGate({required int projectID, required int clinicID}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -206,6 +210,45 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { } } + @override + Future>>> getDoctorsListByHealthCal(int calculationID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapDevice = {"CalculationID": calculationID}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_DOCTOR_LIST_CALCULATION, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + onError!(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_CalculationTable']; + + final doctorsList = list.map((item) => DoctorsListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: doctorsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + @override Future>> getDoctorProfile(int clinicID, int projectID, int doctorId, {Function(dynamic)? onSuccess, Function(String)? onError}) async { Map mapDevice = { @@ -824,7 +867,8 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { } @override - Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { + Future>>> getLaserClinics(int laserCategoryID, int projectID, int languageID, + {Function(dynamic p1)? onSuccess, Function(String p1)? onError}) async { Map mapDevice = { "LaserCategoryID": laserCategoryID, "ProjectID": projectID, @@ -1005,4 +1049,40 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getAppointmentNearestGate({required int projectID, required int clinicID}) async { + Map mapRequest = {"ProjectID": projectID, "ClinicID": clinicID}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_APPOINTMENT_NEAREST_GATE, + body: mapRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final nearestGateResponse = AppointmentNearestGateResponseModel.fromJson(response['getGateByProjectIDandClinicIDList'][0]); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: nearestGateResponse, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index d96cb4f..380d2db 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/LaserCategoryType.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/dental_chief_complaints_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_profile_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; @@ -44,11 +45,18 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isDoctorsListLoading = false; bool isDoctorProfileLoading = false; bool isDoctorSearchByNameStarted = false; + bool isAppointmentNearestGateLoading = false; bool isLiveCareSchedule = false; + bool isGetDocForHealthCal = false; + bool showSortFilterButtons = false; + int? calculationID = 0; + bool isSortByClinic = true; int initialSlotDuration = 0; + bool isNearestAppointmentSelected = false; + LocationUtils locationUtils; List clinicsList = []; @@ -61,6 +69,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { List doctorsList = []; List filteredDoctorList = []; + // Grouped doctors lists + List> doctorsListByClinic = []; + List> doctorsListByHospital = []; + List> doctorsListGrouped = []; + List liveCareDoctorsList = []; List patientDentalPlanEstimationList = []; @@ -122,6 +135,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { PatientAppointmentShareResponseModel? patientWalkInAppointmentShareResponseModel; + AppointmentNearestGateResponseModel? appointmentNearestGateResponseModel; + ///variables for laser clinic List femaleLaserCategory = [ LaserCategoryType(1, 'bodyString'), @@ -143,6 +158,31 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isBodyPartsLoading = false; int duration = 0; + + setIsSortByClinic(bool value) { + isSortByClinic = value; + doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; + notifyListeners(); + } + + // Group doctors by clinic and hospital + void _groupDoctorsList() { + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var doctor in doctorsList) { + final clinicKey = (doctor.clinicName ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(doctor); + + final hospitalKey = (doctor.projectName ?? 'Unknown').trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(doctor); + } + + doctorsListByClinic = clinicMap.values.toList(); + doctorsListByHospital = hospitalMap.values.toList(); + doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; + } + BookAppointmentsViewModel( {required this.bookAppointmentsRepo, required this.errorHandlerService, @@ -161,8 +201,10 @@ class BookAppointmentsViewModel extends ChangeNotifier { void filterClinics(String? query) { if (query!.isEmpty) { _filteredClinicsList = List.from(clinicsList); + showSortFilterButtons = false; } else { _filteredClinicsList = clinicsList.where((clinic) => clinic.clinicDescription?.toLowerCase().contains(query!.toLowerCase()) ?? false).toList(); + showSortFilterButtons = query.length >= 3; } notifyListeners(); } @@ -187,6 +229,18 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsNearestAppointmentSelected(bool isNearestAppointmentSelected) { + this.isNearestAppointmentSelected = isNearestAppointmentSelected; + + if (isNearestAppointmentSelected) { + doctorsList.sort((a, b) => DateUtil.convertStringToDate(a.nearestFreeSlot!).compareTo(DateUtil.convertStringToDate(b.nearestFreeSlot!))); + } else { + doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); + } + + notifyListeners(); + } + setIsWaitingAppointmentSelected(bool isWaitingAppointmentSelected) { this.isWaitingAppointmentSelected = isWaitingAppointmentSelected; notifyListeners(); @@ -280,6 +334,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { } void onTabChanged(int index) { + calculationID = null; + isGetDocForHealthCal = false; selectedTabIndex = index; notifyListeners(); } @@ -319,8 +375,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { Future getLiveCareScheduleClinics({Function(dynamic)? onSuccess, Function(String)? onError}) async { liveCareClinicsList.clear(); - final result = - await bookAppointmentsRepo.getLiveCareScheduleClinics(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!); + final result = await bookAppointmentsRepo.getLiveCareScheduleClinics(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -342,9 +397,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { Future getLiveCareDoctorsList({Function(dynamic)? onSuccess, Function(String)? onError}) async { doctorsList.clear(); - final result = await bookAppointmentsRepo.getLiveCareDoctorsList( - selectedLiveCareClinic.serviceID!, _appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, - onError: onError); + final result = + await bookAppointmentsRepo.getLiveCareDoctorsList(selectedLiveCareClinic.serviceID!, _appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, onError: onError); result.fold( (failure) async { @@ -367,11 +421,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { } //TODO: Make the API dynamic with parameters for ProjectID, isNearest, languageID, doctorId, doctorName - Future getDoctorsList( - {int projectID = 0, bool isNearest = true, int doctorId = 0, - String doctorName = "", - Function(dynamic)? onSuccess, - Function(String)? onError}) async { + Future getDoctorsList({int projectID = 0, bool isNearest = true, int doctorId = 0, String doctorName = "", Function(dynamic)? onSuccess, Function(String)? onError}) async { doctorsList.clear(); projectID = currentlySelectedHospitalFromRegionFlow != null ? int.parse(currentlySelectedHospitalFromRegionFlow!) : projectID; final result = @@ -391,9 +441,43 @@ class BookAppointmentsViewModel extends ChangeNotifier { doctorsList = apiResponse.data!; filteredDoctorList = doctorsList; isDoctorsListLoading = false; + doctorsList.sort((a, b) => b.decimalDoctorRate!.compareTo(a.decimalDoctorRate!)); initializeFilteredList(); clearSearchFilters(); getFiltersFromDoctorList(); + _groupDoctorsList(); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + //TODO: GetDockets & Calculations For Health Calculator + Future getDoctorsListByHealthCal({Function(dynamic)? onSuccess, Function(String)? onError}) async { + doctorsList.clear(); + final result = await bookAppointmentsRepo.getDoctorsListByHealthCal(calculationID!); + result.fold( + (failure) async { + isDoctorsListLoading = false; + if (onError != null) onError("No doctors found for the search criteria".needTranslation); + + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + doctorsList = apiResponse.data!; + setIsSortByClinic(true); + filteredDoctorList = doctorsList; + isDoctorsListLoading = false; + initializeFilteredList(); + clearSearchFilters(); + getFiltersFromDoctorList(); + _groupDoctorsList(); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -404,13 +488,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { } Future getMappedDoctors( - {int projectID = 0, - bool isNearest = false, - int doctorId = 0, - String doctorName = "", - isContinueDentalPlan = false, - Function(dynamic)? onSuccess, - Function(String)? onError}) async { + {int projectID = 0, bool isNearest = false, int doctorId = 0, String doctorName = "", isContinueDentalPlan = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { filteredHospitalList = null; hospitalList = null; isRegionListLoading = true; @@ -446,8 +524,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { } Future getDoctorProfile({Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await bookAppointmentsRepo - .getDoctorProfile(selectedDoctor.clinicID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, onError: onError); + final result = await bookAppointmentsRepo.getDoctorProfile(selectedDoctor.clinicID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, onError: onError); result.fold( (failure) async {}, @@ -506,8 +583,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { // : date = DateUtil.convertStringToDateSaudiTimezone(element, int.parse(selectedDoctor.projectID.toString())); slotsList.add(FreeSlot(date, ['slot'])); - docFreeSlots.add(TimeSlot( - isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element)); + docFreeSlots.add(TimeSlot(isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element)); }); notifyListeners(); @@ -526,8 +602,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); Map _eventsParsed; - final result = await bookAppointmentsRepo.getLiveCareDoctorFreeSlots(selectedDoctor.clinicID ?? 0, selectedLiveCareClinic.serviceID ?? 0, - selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, isBookingForLiveCare, + final result = await bookAppointmentsRepo.getLiveCareDoctorFreeSlots( + selectedDoctor.clinicID ?? 0, selectedLiveCareClinic.serviceID ?? 0, selectedDoctor.projectID ?? 0, selectedDoctor.doctorID ?? 0, isBookingForLiveCare, onError: onError); result.fold( @@ -551,8 +627,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { // : date = DateUtil.convertStringToDateSaudiTimezone(element, int.parse(selectedDoctor.projectID.toString())); slotsList.add(FreeSlot(date, ['slot'])); - docFreeSlots.add(TimeSlot( - isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element)); + docFreeSlots.add(TimeSlot(isoTime: formatter.format(date), start: new DateTime(date.year, date.month, date.day, 0, 0, 0, 0), end: date, vidaDate: element)); }); notifyListeners(); @@ -564,10 +639,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { ); } - Future cancelAppointment( - {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, - Function(dynamic)? onSuccess, - Function(String)? onError}) async { + Future cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await bookAppointmentsRepo.cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); result.fold( @@ -651,15 +723,13 @@ class BookAppointmentsViewModel extends ChangeNotifier { await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async { navigationService.pop(); Future.delayed(Duration(milliseconds: 50)).then((value) async {}); - LoadingUtils.showFullScreenLoader( - barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation); + LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation); await insertSpecificAppointment( onError: (err) {}, onSuccess: (apiResp) async { LoadingUtils.hideFullScreenLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { - LoadingUtils.showFullScreenLoader( - barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); + LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); await Future.delayed(Duration(milliseconds: 4000)).then((value) { LoadingUtils.hideFullScreenLoader(); Navigator.pushAndRemoveUntil( @@ -749,15 +819,13 @@ class BookAppointmentsViewModel extends ChangeNotifier { await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async { navigationService.pop(); Future.delayed(Duration(milliseconds: 50)).then((value) async {}); - LoadingUtils.showFullScreenLoader( - barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation); + LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: "Booking your appointment...".needTranslation); await insertSpecificAppointment( onError: (err) {}, onSuccess: (apiResp) async { LoadingUtils.hideFullScreenLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { - LoadingUtils.showFullScreenLoader( - barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); + LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); await Future.delayed(Duration(milliseconds: 4000)).then((value) { LoadingUtils.hideFullScreenLoader(); Navigator.pushAndRemoveUntil( @@ -831,9 +899,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { } else { filteredHospitalList = RegionList(); - var list = isHMG - ? hospitalList?.registeredDoctorMap![selectedRegionId]!.hmgDoctorList - : hospitalList?.registeredDoctorMap![selectedRegionId]!.hmcDoctorList; + var list = isHMG ? hospitalList?.registeredDoctorMap![selectedRegionId]!.hmgDoctorList : hospitalList?.registeredDoctorMap![selectedRegionId]!.hmcDoctorList; if (list != null && list.isEmpty) { notifyListeners(); @@ -916,8 +982,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } - void setSelections(List? selectedFacilityForFilters, List? selectedRegionForFilters, String? selectedClinicForFilters, - PatientDoctorAppointmentList? selectedHospitalForFilters, bool applyFilters) { + void setSelections( + List? selectedFacilityForFilters, List? selectedRegionForFilters, String? selectedClinicForFilters, PatientDoctorAppointmentList? selectedHospitalForFilters, bool applyFilters) { this.selectedFacilityForFilters = selectedFacilityForFilters; this.selectedClinicForFilters = selectedClinicForFilters; this.selectedHospitalForFilters = selectedHospitalForFilters; @@ -985,15 +1051,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { List getDoctorListAsPerSelection() { if (!applyFilters) return doctorsList; - if ((selectedRegionForFilters?.isEmpty == true) && - (selectedFacilityForFilters?.isEmpty == true) && - selectedClinicForFilters == null && - selectedHospitalForFilters == null) { + if ((selectedRegionForFilters?.isEmpty == true) && (selectedFacilityForFilters?.isEmpty == true) && selectedClinicForFilters == null && selectedHospitalForFilters == null) { return doctorsList; } var list = doctorsList.where((element) { - var isInSelectedRegion = - (selectedRegionForFilters?.isEmpty == true) ? true : selectedRegionForFilters?.any((region) => region == element.getRegionName(isArabic())); + var isInSelectedRegion = (selectedRegionForFilters?.isEmpty == true) ? true : selectedRegionForFilters?.any((region) => region == element.getRegionName(isArabic())); var shouldApplyFacilityFilter = (selectedFacilityForFilters?.isEmpty == true) ? false : true; var isHMC = (selectedFacilityForFilters?.isEmpty == true) ? true : selectedFacilityForFilters?.any((item) => item.contains("hmc")); var isInSelectedClinic = (selectedClinicForFilters == null) ? true : selectedClinicForFilters == element.clinicName; @@ -1044,8 +1106,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { dentalChiefComplaintsList.clear(); notifyListeners(); int patientID = _appState.isAuthenticated ? _appState.getAuthenticatedUser()!.patientId ?? -1 : -1; - final result = await bookAppointmentsRepo.getDentalChiefComplaintsList( - patientID: patientID, projectID: int.parse(currentlySelectedHospitalFromRegionFlow ?? "0"), clinicID: 17); + final result = await bookAppointmentsRepo.getDentalChiefComplaintsList(patientID: patientID, projectID: int.parse(currentlySelectedHospitalFromRegionFlow ?? "0"), clinicID: 17); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -1084,6 +1145,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { // initializeFilteredList(); // clearSearchFilters(); // getFiltersFromDoctorList(); + _groupDoctorsList(); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -1288,4 +1350,32 @@ class BookAppointmentsViewModel extends ChangeNotifier { }, ); } + + Future getAppointmentNearestGate({required int projectID, required int clinicID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + isAppointmentNearestGateLoading = true; + notifyListeners(); + + final result = await bookAppointmentsRepo.getAppointmentNearestGate(projectID: projectID, clinicID: clinicID); + + result.fold( + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + appointmentNearestGateResponseModel = apiResponse.data!; + isAppointmentNearestGateLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart b/lib/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart new file mode 100644 index 0000000..bdaa4e2 --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/appointment_nearest_gate_response_model.dart @@ -0,0 +1,64 @@ +class AppointmentNearestGateResponseModel { + String? clinicDescription; + String? clinicDescriptionN; + int? clinicID; + String? clinicLocation; + String? clinicLocationN; + int? gender; + int? iD; + String? nearestGateNumber; + String? nearestGateNumberN; + int? projectID; + String? projectName; + String? projectNameN; + int? rowID; + + AppointmentNearestGateResponseModel( + {this.clinicDescription, + this.clinicDescriptionN, + this.clinicID, + this.clinicLocation, + this.clinicLocationN, + this.gender, + this.iD, + this.nearestGateNumber, + this.nearestGateNumberN, + this.projectID, + this.projectName, + this.projectNameN, + this.rowID}); + + AppointmentNearestGateResponseModel.fromJson(Map json) { + clinicDescription = json['ClinicDescription']; + clinicDescriptionN = json['ClinicDescriptionN']; + clinicID = json['ClinicID']; + clinicLocation = json['ClinicLocation']; + clinicLocationN = json['ClinicLocationN']; + gender = json['Gender']; + iD = json['ID']; + nearestGateNumber = json['NearestGateNumber']; + nearestGateNumberN = json['NearestGateNumberN']; + projectID = json['ProjectID']; + projectName = json['ProjectName']; + projectNameN = json['ProjectNameN']; + rowID = json['RowID']; + } + + Map toJson() { + final Map data = Map(); + data['ClinicDescription'] = clinicDescription; + data['ClinicDescriptionN'] = clinicDescriptionN; + data['ClinicID'] = clinicID; + data['ClinicLocation'] = clinicLocation; + data['ClinicLocationN'] = clinicLocationN; + data['Gender'] = gender; + data['ID'] = iD; + data['NearestGateNumber'] = nearestGateNumber; + data['NearestGateNumberN'] = nearestGateNumberN; + data['ProjectID'] = projectID; + data['ProjectName'] = projectName; + data['ProjectNameN'] = projectNameN; + data['RowID'] = rowID; + return data; + } +} diff --git a/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart b/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart index 5df6ed8..75ed122 100644 --- a/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart +++ b/lib/features/book_appointments/models/resp_models/doctors_list_response_model.dart @@ -65,75 +65,77 @@ class DoctorsListResponseModel { String? regionID; String? projectBottomName; String? projectTopName; + int? calcID; - - DoctorsListResponseModel( - {this.clinicID, - this.clinicName, - this.clinicNameN, - this.doctorTitle, - this.iD, - this.name, - this.projectID, - this.projectName, - this.actualDoctorRate, - this.clinicRoomNo, - this.date, - this.dayName, - this.decimalDoctorRate, - this.doctorAvailability, - this.doctorID, - this.doctorImageURL, - this.doctorMobileNumber, - this.doctorProfile, - this.doctorProfileInfo, - this.doctorRate, - this.doctorStarsRate, - this.employmentType, - this.gender, - this.genderDescription, - this.hISRegionId, - this.isActive, - this.isAllowWaitList, - this.isAppointmentAllowed, - this.isDoctorAllowVedioCall, - this.isDoctorDummy, - this.isDoctorHasPrePostImages, - this.isHMC, - this.isHmg, - this.isLiveCare, - this.latitude, - this.longitude, - this.nationalityFlagURL, - this.nationalityID, - this.nationalityName, - this.nearestFreeSlot, - this.noOfFreeSlotsAvailable, - this.noOfPatientsRate, - this.originalClinicID, - this.personRate, - this.projectDistanceInKiloMeters, - this.projectNameBottom, - this.projectNameTop, - this.qR, - this.qRString, - this.rateNumber, - this.regionName, - this.regionNameN, - this.serviceID, - this.setupID, - this.speciality, - this.specialityN, - this.transactionType, - this.virtualEmploymentType, - this.workingHours, - this.vida3Id, - this.region, - this.regionArabic, - this.regionEnglish, - this.regionID, - this.projectBottomName, - this.projectTopName,}); + DoctorsListResponseModel({ + this.clinicID, + this.clinicName, + this.clinicNameN, + this.doctorTitle, + this.iD, + this.name, + this.projectID, + this.projectName, + this.actualDoctorRate, + this.clinicRoomNo, + this.date, + this.dayName, + this.decimalDoctorRate, + this.doctorAvailability, + this.doctorID, + this.doctorImageURL, + this.doctorMobileNumber, + this.doctorProfile, + this.doctorProfileInfo, + this.doctorRate, + this.doctorStarsRate, + this.employmentType, + this.gender, + this.genderDescription, + this.hISRegionId, + this.isActive, + this.isAllowWaitList, + this.isAppointmentAllowed, + this.isDoctorAllowVedioCall, + this.isDoctorDummy, + this.isDoctorHasPrePostImages, + this.isHMC, + this.isHmg, + this.isLiveCare, + this.latitude, + this.longitude, + this.nationalityFlagURL, + this.nationalityID, + this.nationalityName, + this.nearestFreeSlot, + this.noOfFreeSlotsAvailable, + this.noOfPatientsRate, + this.originalClinicID, + this.personRate, + this.projectDistanceInKiloMeters, + this.projectNameBottom, + this.projectNameTop, + this.qR, + this.qRString, + this.rateNumber, + this.regionName, + this.regionNameN, + this.serviceID, + this.setupID, + this.speciality, + this.specialityN, + this.transactionType, + this.virtualEmploymentType, + this.workingHours, + this.vida3Id, + this.region, + this.regionArabic, + this.regionEnglish, + this.regionID, + this.projectBottomName, + this.projectTopName, + this.calcID, + }); DoctorsListResponseModel.fromJson(Map json) { clinicID = json['ClinicID']; @@ -141,7 +143,7 @@ class DoctorsListResponseModel { clinicNameN = json['ClinicNameN']; doctorTitle = json['DoctorTitle']; iD = json['ID']; - name = json['Name']; + name = json['Name'] ?? json["DoctorName"]; projectID = json['ProjectID']; projectName = json['ProjectName']; actualDoctorRate = json['ActualDoctorRate']; @@ -174,7 +176,7 @@ class DoctorsListResponseModel { longitude = json['Longitude']; nationalityFlagURL = json['NationalityFlagURL']; nationalityID = json['NationalityID']; - nationalityName = json['NationalityName']; + nationalityName = json['NationalityName'] ?? json["Nationality"]; nearestFreeSlot = json['NearestFreeSlot']; noOfFreeSlotsAvailable = json['NoOfFreeSlotsAvailable']; noOfPatientsRate = json['NoOfPatientsRate']; @@ -200,6 +202,7 @@ class DoctorsListResponseModel { regionEnglish = json['RegionName']; projectBottomName = json['ProjectNameBottom']; projectTopName = json['ProjectNameTop']; + calcID = json["CalcID"]; } Map toJson() { @@ -264,6 +267,7 @@ class DoctorsListResponseModel { data['VirtualEmploymentType'] = this.virtualEmploymentType; data['WorkingHours'] = this.workingHours; data['vida3Id'] = this.vida3Id; + data['CalcID'] = this.calcID; return data; } @@ -273,7 +277,8 @@ class DoctorsListResponseModel { } return regionEnglish; } - String getProjectCompleteName(){ + + String getProjectCompleteName() { return "${this.projectTopName} ${this.projectBottomName}"; } diff --git a/lib/features/health_trackers/health_trackers_repo.dart b/lib/features/health_trackers/health_trackers_repo.dart new file mode 100644 index 0000000..2a64fc4 --- /dev/null +++ b/lib/features/health_trackers/health_trackers_repo.dart @@ -0,0 +1,873 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Progress types to request different ranges from the progress API. +enum ProgressType { today, week, month } + +abstract class HealthTrackersRepo { + // ==================== BLOOD SUGAR (DIABETIC) ==================== + /// Get blood sugar result averages (week, month, year). + Future>> getDiabeticResultAverage(); + + /// Get blood sugar results (week, month, year). + Future>> getDiabeticResults(); + + /// Add new blood sugar result. + Future>> addDiabeticResult({ + required String bloodSugarDateChart, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + }); + + /// Update existing blood sugar result. + Future>> updateDiabeticResult({ + required DateTime month, + required DateTime hour, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + required int lineItemNo, + }); + + /// Deactivate blood sugar record. + Future>> deactivateDiabeticStatus({ + required int lineItemNo, + }); + + /// Send blood sugar report by email. + Future>> sendBloodSugarReportByEmail({ + required String email, + }); + + // ==================== BLOOD PRESSURE ==================== + /// Get blood pressure result averages (week, month, year). + Future>> getBloodPressureResultAverage(); + + /// Get blood pressure results (week, month, year). + Future>> getBloodPressureResults(); + + /// Add new blood pressure result. + Future>> addBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + }); + + /// Update existing blood pressure result. + Future>> updateBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + required int lineItemNo, + }); + + /// Deactivate blood pressure record. + Future>> deactivateBloodPressureStatus({ + required int lineItemNo, + }); + + /// Send blood pressure report by email. + Future>> sendBloodPressureReportByEmail({ + required String email, + }); + + // ==================== WEIGHT MEASUREMENT ==================== + /// Get weight measurement result averages (week, month, year). + Future>> getWeightMeasurementResultAverage(); + + /// Get weight measurement results (week, month, year). + Future>> getWeightMeasurementResults(); + + /// Add new weight measurement result. + Future>> addWeightMeasurementResult({ + required String weightDate, + required String weightMeasured, + required int weightUnit, + }); + + /// Update existing weight measurement result. + Future>> updateWeightMeasurementResult({ + required int lineItemNo, + required int weightUnit, + required String weightMeasured, + required String weightDate, + }); + + /// Deactivate weight measurement record. + Future>> deactivateWeightMeasurementStatus({ + required int lineItemNo, + }); + + /// Send weight report by email. + Future>> sendWeightReportByEmail({ + required String email, + }); +} + +class HealthTrackersRepoImp implements HealthTrackersRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + HealthTrackersRepoImp({required this.loggerService, required this.apiClient}); + + // ==================== BLOOD SUGAR (DIABETIC) METHODS ==================== + + @override + Future>> getDiabeticResultAverage() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getDiabeticResultAverage, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract average lists + extracted = { + 'monthAverageList': response['List_MonthDiabtectResultAverage'] ?? [], + 'weekAverageList': response['List_WeekDiabtectResultAverage'] ?? [], + 'yearAverageList': response['List_YearDiabtecResultAverage'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getDiabeticResults() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getDiabeticResult, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract patient result lists + extracted = { + 'monthResultList': response['List_MonthDiabtecPatientResult'] ?? [], + 'weekResultList': response['List_WeekDiabtecPatientResult'] ?? [], + 'yearResultList': response['List_YearDiabtecPatientResult'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addDiabeticResult({ + required String bloodSugarDateChart, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'BloodSugerDateChart': bloodSugarDateChart, + 'BloodSugerResult': bloodSugarResult, + 'DiabtecUnit': diabeticUnit, + 'MeasuredTime': measuredTime + 1, // Add 1 as per old service + }; + + await apiClient.post( + ApiConsts.addDiabeticResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateDiabeticResult({ + required DateTime month, + required DateTime hour, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + // Format: 'YYYY-MM-DD HH:MM:SS' as per old service + String formattedDate = '${month.year}-${month.month}-${month.day} ${hour.hour}:${hour.minute}:00'; + + Map body = { + 'BloodSugerDateChart': formattedDate, + 'BloodSugerResult': bloodSugarResult, + 'DiabtecUnit': diabeticUnit, + 'MeasuredTime': measuredTime + 1, // Add 1 as per old service + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.updateDiabeticResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> deactivateDiabeticStatus({ + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.deactivateDiabeticStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> sendBloodSugarReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBloodSugarReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + // ==================== BLOOD PRESSURE METHODS ==================== + + @override + Future>> getBloodPressureResultAverage() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBloodPressureResultAverage, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract all three list types + extracted = { + 'monthList': response['List_MonthBloodPressureResultAverage'] ?? [], + 'weekList': response['List_WeekBloodPressureResultAverage'] ?? [], + 'yearList': response['List_YearBloodPressureResultAverage'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getBloodPressureResults() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBloodPressureResult, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract all three list types + extracted = { + 'weekList': response['List_WeekBloodPressureResult'] ?? [], + 'monthList': response['List_MonthBloodPressureResult'] ?? [], + 'yearList': response['List_YearBloodPressureResult'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'BloodPressureDate': bloodPressureDate, + 'DiastolicPressure': diastolicPressure, + 'SystolicePressure': systolicePressure, + 'MeasuredArm': measuredArm, + }; + + await apiClient.post( + ApiConsts.addBloodPressureResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'BloodPressureDate': bloodPressureDate, + 'DiastolicPressure': diastolicPressure, + 'SystolicePressure': systolicePressure, + 'MeasuredArm': measuredArm, + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.updateBloodPressureResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> deactivateBloodPressureStatus({ + required int lineItemNo, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.deactivateBloodPressureStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> sendBloodPressureReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBloodPressureReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + // ==================== WEIGHT MEASUREMENT METHODS ==================== + + @override + Future>> getWeightMeasurementResultAverage() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getWeightMeasurementResultAverage, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract average lists + extracted = { + 'monthAverageList': response['List_MonthWeightMeasurementResultAverage'] ?? [], + 'weekAverageList': response['List_WeekWeightMeasurementResultAverage'] ?? [], + 'yearAverageList': response['List_YearWeightMeasurementResultAverage'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getWeightMeasurementResults() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getWeightMeasurementResult, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map) { + // Extract result lists + extracted = { + 'weekResultList': response['List_WeekWeightMeasurementResult'] ?? [], + 'monthResultList': response['List_MonthWeightMeasurementResult'] ?? [], + 'yearResultList': response['List_YearWeightMeasurementResult'] ?? [], + }; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> addWeightMeasurementResult({ + required String weightDate, + required String weightMeasured, + required int weightUnit, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'WeightDate': weightDate, + 'WeightMeasured': weightMeasured, + 'weightUnit': weightUnit, + }; + + await apiClient.post( + ApiConsts.addWeightMeasurementResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateWeightMeasurementResult({ + required int lineItemNo, + required int weightUnit, + required String weightMeasured, + required String weightDate, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + 'weightUnit': '$weightUnit', // Convert to string as per old service + 'WeightMeasured': weightMeasured, + 'WeightDate': weightDate, + }; + + await apiClient.post( + ApiConsts.updateWeightMeasurementResult, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> deactivateWeightMeasurementStatus({required int lineItemNo}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'LineItemNo': lineItemNo, + }; + + await apiClient.post( + ApiConsts.deactivateWeightMeasurementStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> sendWeightReportByEmail({ + required String email, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + Map body = { + 'To': email, + }; + + await apiClient.post( + ApiConsts.sendAverageBodyWeightReport, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart b/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart new file mode 100644 index 0000000..f024d8e --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/blood_pressure_result.dart @@ -0,0 +1,87 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class BloodPressureResult { + int? patientID; + int? lineItemNo; + DateTime? bloodPressureDate; + int? measuredArm; + int? systolicePressure; + int? diastolicPressure; + dynamic remark; + bool? isActive; + int? chartYear; + String? chartMonth; + dynamic yearSystolicePressureAverageResult; + dynamic monthSystolicePressureResult; + dynamic weekSystolicePressureResult; + int? yearDiastolicPressureAverageResult; + dynamic monthDiastolicPressureResult; + dynamic weekDiastolicPressureResult; + String? measuredArmDesc; + dynamic weekDesc; + + BloodPressureResult( + {this.patientID, + this.lineItemNo, + this.bloodPressureDate, + this.measuredArm, + this.systolicePressure, + this.diastolicPressure, + this.remark, + this.isActive, + this.chartYear, + this.chartMonth, + this.yearSystolicePressureAverageResult, + this.monthSystolicePressureResult, + this.weekSystolicePressureResult, + this.yearDiastolicPressureAverageResult, + this.monthDiastolicPressureResult, + this.weekDiastolicPressureResult, + this.measuredArmDesc, + this.weekDesc}); + + BloodPressureResult.fromJson(Map json) { + patientID = json['PatientID']; + lineItemNo = json['LineItemNo']; + bloodPressureDate = DateUtil.convertStringToDate(json['BloodPressureDate']); + measuredArm = json['MeasuredArm']; + systolicePressure = json['SystolicePressure']; + diastolicPressure = json['DiastolicPressure']; + remark = json['Remark']; + isActive = json['IsActive']; + chartYear = json['ChartYear']; + chartMonth = json['ChartMonth']; + yearSystolicePressureAverageResult = json['YearSystolicePressureAverageResult']; + monthSystolicePressureResult = json['MonthSystolicePressureResult']; + weekSystolicePressureResult = json['WeekSystolicePressureResult']; + yearDiastolicPressureAverageResult = json['YearDiastolicPressureAverageResult']; + monthDiastolicPressureResult = json['MonthDiastolicPressureResult']; + weekDiastolicPressureResult = json['WeekDiastolicPressureResult']; + measuredArmDesc = json['MeasuredArmDesc']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['PatientID'] = patientID; + data['LineItemNo'] = lineItemNo; + data['BloodPressureDate'] = bloodPressureDate; + data['MeasuredArm'] = measuredArm; + data['SystolicePressure'] = systolicePressure; + data['DiastolicPressure'] = diastolicPressure; + data['Remark'] = remark; + data['IsActive'] = isActive; + data['ChartYear'] = chartYear; + data['ChartMonth'] = chartMonth; + data['YearSystolicePressureAverageResult'] = yearSystolicePressureAverageResult; + data['MonthSystolicePressureResult'] = monthSystolicePressureResult; + data['WeekSystolicePressureResult'] = weekSystolicePressureResult; + data['YearDiastolicPressureAverageResult'] = yearDiastolicPressureAverageResult; + data['MonthDiastolicPressureResult'] = monthDiastolicPressureResult; + data['WeekDiastolicPressureResult'] = weekDiastolicPressureResult; + data['MeasuredArmDesc'] = measuredArmDesc; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart new file mode 100644 index 0000000..3975805 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart @@ -0,0 +1,58 @@ +class MonthBloodPressureResultAverage { + dynamic weekfourSystolicePressureAverageResult; + dynamic weekfourDiastolicPressureAverageResult; + dynamic weekthreeSystolicePressureAverageResult; + dynamic weekthreeDiastolicPressureAverageResult; + dynamic weektwoSystolicePressureAverageResult; + dynamic weektwoDiastolicPressureAverageResult; + dynamic weekoneSystolicePressureAverageResult; + dynamic weekoneDiastolicPressureAverageResult; + String? weekDesc; + int? weekDiastolicPressureAverageResult; + int? weekSystolicePressureAverageResult; + + MonthBloodPressureResultAverage({ + this.weekfourSystolicePressureAverageResult, + this.weekfourDiastolicPressureAverageResult, + this.weekthreeSystolicePressureAverageResult, + this.weekthreeDiastolicPressureAverageResult, + this.weektwoSystolicePressureAverageResult, + this.weektwoDiastolicPressureAverageResult, + this.weekoneSystolicePressureAverageResult, + this.weekoneDiastolicPressureAverageResult, + this.weekDesc, + this.weekDiastolicPressureAverageResult, + this.weekSystolicePressureAverageResult, + }); + + MonthBloodPressureResultAverage.fromJson(Map json) { + weekfourSystolicePressureAverageResult = json['weekfourSystolicePressureAverageResult']; + weekfourDiastolicPressureAverageResult = json['weekfourDiastolicPressureAverageResult']; + weekthreeSystolicePressureAverageResult = json['weekthreeSystolicePressureAverageResult']; + weekthreeDiastolicPressureAverageResult = json['weekthreeDiastolicPressureAverageResult']; + weektwoSystolicePressureAverageResult = json['weektwoSystolicePressureAverageResult']; + weektwoDiastolicPressureAverageResult = json['weektwoDiastolicPressureAverageResult']; + weekoneSystolicePressureAverageResult = json['weekoneSystolicePressureAverageResult']; + weekoneDiastolicPressureAverageResult = json['weekoneDiastolicPressureAverageResult']; + weekDesc = json['WeekDesc']; + weekDiastolicPressureAverageResult = json['WeekDiastolicPressureAverageResult']; + weekSystolicePressureAverageResult = json['WeekSystolicePressureAverageResult']; + } + + Map toJson() { + final Map data = {}; + data['weekfourSystolicePressureAverageResult'] = weekfourSystolicePressureAverageResult; + data['weekfourDiastolicPressureAverageResult'] = weekfourDiastolicPressureAverageResult; + data['weekthreeSystolicePressureAverageResult'] = weekthreeSystolicePressureAverageResult; + data['weekthreeDiastolicPressureAverageResult'] = weekthreeDiastolicPressureAverageResult; + data['weektwoSystolicePressureAverageResult'] = weektwoSystolicePressureAverageResult; + data['weektwoDiastolicPressureAverageResult'] = weektwoDiastolicPressureAverageResult; + data['weekoneSystolicePressureAverageResult'] = weekoneSystolicePressureAverageResult; + data['weekoneDiastolicPressureAverageResult'] = weekoneDiastolicPressureAverageResult; + data['WeekDesc'] = weekDesc; + data['WeekDiastolicPressureAverageResult'] = weekDiastolicPressureAverageResult; + data['WeekSystolicePressureAverageResult'] = weekSystolicePressureAverageResult; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart new file mode 100644 index 0000000..381d514 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart @@ -0,0 +1,24 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekBloodPressureResultAverage { + int? dailySystolicePressureAverageResult; + int? dailyDiastolicPressureAverageResult; + DateTime? bloodPressureDate; + + WeekBloodPressureResultAverage({this.dailySystolicePressureAverageResult, this.dailyDiastolicPressureAverageResult, this.bloodPressureDate}); + + WeekBloodPressureResultAverage.fromJson(Map json) { + dailySystolicePressureAverageResult = json['DailySystolicePressureAverageResult']; + dailyDiastolicPressureAverageResult = json['DailyDiastolicPressureAverageResult']; + bloodPressureDate = DateUtil.convertStringToDate(json['BloodPressureDate']); + } + + Map toJson() { + final Map data = {}; + data['DailySystolicePressureAverageResult'] = dailySystolicePressureAverageResult; + data['DailyDiastolicPressureAverageResult'] = dailyDiastolicPressureAverageResult; + data['BloodPressureDate'] = bloodPressureDate; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart b/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart new file mode 100644 index 0000000..6f34246 --- /dev/null +++ b/lib/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart @@ -0,0 +1,37 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearBloodPressureResultAverage { + int? monthSystolicePressureAverageResult; + int? monthDiastolicPressureAverageResult; + dynamic monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearBloodPressureResultAverage({ + this.monthSystolicePressureAverageResult, + this.monthDiastolicPressureAverageResult, + this.monthNumber, + this.monthName, + this.yearName, + }); + + YearBloodPressureResultAverage.fromJson(Map json) { + monthSystolicePressureAverageResult = json['monthSystolicePressureAverageResult']; + monthDiastolicPressureAverageResult = json['monthDiastolicPressureAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } + + Map toJson() { + final Map data = {}; + data['monthSystolicePressureAverageResult'] = monthSystolicePressureAverageResult; + data['monthDiastolicPressureAverageResult'] = monthDiastolicPressureAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} diff --git a/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart b/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart new file mode 100644 index 0000000..066df3c --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart @@ -0,0 +1,99 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class DiabeticPatientResult { + String? chartMonth; + var chartYear; + DateTime? dateChart; + var description; + var descriptionN; + int? diabtecAvarage; + bool? isActive; + int? lineItemNo; + var listMonth; + var listWeek; + int? measured; + String? measuredDesc; + var monthAverageResult; + int? patientID; + var remark; + var resultDesc; + dynamic resultValue; + String? unit; + var weekAverageResult; + String? weekDesc; + var yearAverageResult; + + DiabeticPatientResult( + {this.chartMonth, + this.chartYear, + this.dateChart, + this.description, + this.descriptionN, + this.diabtecAvarage, + this.isActive, + this.lineItemNo, + this.listMonth, + this.listWeek, + this.measured, + this.measuredDesc, + this.monthAverageResult, + this.patientID, + this.remark, + this.resultDesc, + this.resultValue, + this.unit, + this.weekAverageResult, + this.weekDesc, + this.yearAverageResult}); + + DiabeticPatientResult.fromJson(Map json) { + chartMonth = json['ChartMonth']; + chartYear = json['ChartYear']; + dateChart = DateUtil.convertStringToDate(json['DateChart']); + description = json['Description']; + descriptionN = json['DescriptionN']; + diabtecAvarage = json['DiabtecAvarage']; + isActive = json['IsActive']; + lineItemNo = json['LineItemNo']; + listMonth = json['List_Month']; + listWeek = json['List_Week']; + measured = json['Measured']; + measuredDesc = json['MeasuredDesc']; + monthAverageResult = json['MonthAverageResult']; + patientID = json['PatientID']; + remark = json['Remark']; + resultDesc = json['ResultDesc']; + resultValue = json['ResultValue']; + unit = json['Unit']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + yearAverageResult = json['YearAverageResult']; + } + + Map toJson() { + final Map data = {}; + data['ChartMonth'] = chartMonth; + data['ChartYear'] = chartYear; + data['DateChart'] = DateUtil.convertDateToString(dateChart!); + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['DiabtecAvarage'] = diabtecAvarage; + data['IsActive'] = isActive; + data['LineItemNo'] = lineItemNo; + data['List_Month'] = listMonth; + data['List_Week'] = listWeek; + data['Measured'] = measured; + data['MeasuredDesc'] = measuredDesc; + data['MonthAverageResult'] = monthAverageResult; + data['PatientID'] = patientID; + data['Remark'] = remark; + data['ResultDesc'] = resultDesc; + data['ResultValue'] = resultValue; + data['Unit'] = unit; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + data['YearAverageResult'] = yearAverageResult; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart new file mode 100644 index 0000000..77b06f1 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart @@ -0,0 +1,37 @@ +class MonthDiabeticResultAverage { + var weekfourAverageResult; + var weekthreeAverageResult; + var weektwoAverageResult; + var weekoneAverageResult; + dynamic weekAverageResult; + String? weekDesc; + + MonthDiabeticResultAverage( + {this.weekfourAverageResult, + this.weekthreeAverageResult, + this.weektwoAverageResult, + this.weekoneAverageResult, + this.weekAverageResult, + this.weekDesc}); + + MonthDiabeticResultAverage.fromJson(Map json) { + weekfourAverageResult = json['weekfourAverageResult']; + weekthreeAverageResult = json['weekthreeAverageResult']; + weektwoAverageResult = json['weektwoAverageResult']; + weekoneAverageResult = json['weekoneAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['weekfourAverageResult'] = weekfourAverageResult; + data['weekthreeAverageResult'] = weekthreeAverageResult; + data['weektwoAverageResult'] = weektwoAverageResult; + data['weekoneAverageResult'] = weekoneAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart new file mode 100644 index 0000000..3b35fe7 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart @@ -0,0 +1,21 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekDiabeticResultAverage { + dynamic dailyAverageResult; + DateTime? dateChart; + + WeekDiabeticResultAverage({this.dailyAverageResult, this.dateChart}); + + WeekDiabeticResultAverage.fromJson(Map json) { + dailyAverageResult = json['DailyAverageResult']; + dateChart = DateUtil.convertStringToDate(json['DateChart']); + } + + Map toJson() { + final Map data = {}; + data['DailyAverageResult'] = dailyAverageResult; + data['DateChart'] = DateUtil.convertDateToString(dateChart!); + return data; + } +} + diff --git a/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart b/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart new file mode 100644 index 0000000..fb34056 --- /dev/null +++ b/lib/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart @@ -0,0 +1,35 @@ +import 'dart:developer'; + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearDiabeticResultAverage { + dynamic monthAverageResult; + var monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearDiabeticResultAverage({this.monthAverageResult, this.monthNumber, this.monthName, this.yearName}); + + YearDiabeticResultAverage.fromJson(Map json) { + try { + monthAverageResult = json['monthAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } catch (e) { + log(e.toString()); + } + } + + Map toJson() { + final Map data = {}; + data['monthAverageResult'] = monthAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart new file mode 100644 index 0000000..f95470c --- /dev/null +++ b/lib/features/health_trackers/models/weight/month_weight_measurement_result_average.dart @@ -0,0 +1,37 @@ +class MonthWeightMeasurementResultAverage { + dynamic weekfourAverageResult; + dynamic weekthreeAverageResult; + dynamic weektwoAverageResult; + dynamic weekoneAverageResult; + dynamic weekAverageResult; + String? weekDesc; + + MonthWeightMeasurementResultAverage( + {this.weekfourAverageResult, + this.weekthreeAverageResult, + this.weektwoAverageResult, + this.weekoneAverageResult, + this.weekAverageResult, + this.weekDesc}); + + MonthWeightMeasurementResultAverage.fromJson(Map json) { + weekfourAverageResult = json['weekfourAverageResult']; + weekthreeAverageResult = json['weekthreeAverageResult']; + weektwoAverageResult = json['weektwoAverageResult']; + weekoneAverageResult = json['weekoneAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } + + Map toJson() { + final Map data = {}; + data['weekfourAverageResult'] = weekfourAverageResult; + data['weekthreeAverageResult'] = weekthreeAverageResult; + data['weektwoAverageResult'] = weektwoAverageResult; + data['weekoneAverageResult'] = weekoneAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart new file mode 100644 index 0000000..ad53325 --- /dev/null +++ b/lib/features/health_trackers/models/weight/week_weight_measurement_result_average.dart @@ -0,0 +1,21 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeekWeightMeasurementResultAverage { + dynamic dailyAverageResult; + DateTime? weightDate; + + WeekWeightMeasurementResultAverage({this.dailyAverageResult, this.weightDate}); + + WeekWeightMeasurementResultAverage.fromJson(Map json) { + dailyAverageResult = json['DailyAverageResult']; + weightDate = DateUtil.convertStringToDate(json['WeightDate']); + } + + Map toJson() { + final Map data = {}; + data['DailyAverageResult'] = dailyAverageResult; + data['WeightDate'] = weightDate; + return data; + } +} + diff --git a/lib/features/health_trackers/models/weight/weight_measurement_result.dart b/lib/features/health_trackers/models/weight/weight_measurement_result.dart new file mode 100644 index 0000000..896d33f --- /dev/null +++ b/lib/features/health_trackers/models/weight/weight_measurement_result.dart @@ -0,0 +1,77 @@ +import 'dart:developer'; + +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class WeightMeasurementResult { + int? patientID; + int? lineItemNo; + int? weightMeasured; + DateTime? weightDate; + dynamic remark; + bool? isActive; + int? measured; + dynamic unit; + int? chartYear; + dynamic chartMonth; + double? yearAverageResult; + dynamic monthAverageResult; + dynamic weekAverageResult; + dynamic weekDesc; + + WeightMeasurementResult( + {this.patientID, + this.lineItemNo, + this.weightMeasured, + this.weightDate, + this.remark, + this.isActive, + this.measured, + this.unit, + this.chartYear, + this.chartMonth, + this.yearAverageResult, + this.monthAverageResult, + this.weekAverageResult, + this.weekDesc}); + + WeightMeasurementResult.fromJson(Map json) { + try { + patientID = json['PatientID']; + lineItemNo = json['LineItemNo']; + weightMeasured = json['WeightMeasured']; + weightDate = DateUtil.convertStringToDate(json['WeightDate']); + remark = json['Remark']; + isActive = json['IsActive']; + measured = json['Measured']; + unit = json['Unit']; + chartYear = json['ChartYear']; + chartMonth = json['ChartMonth']; + // Convert to double safely since API may return int + yearAverageResult = json['YearAverageResult'] != null ? (json['YearAverageResult'] as num).toDouble() : null; + monthAverageResult = json['MonthAverageResult']; + weekAverageResult = json['WeekAverageResult']; + weekDesc = json['WeekDesc']; + } catch (e) { + log(e.toString()); + } + } + + Map toJson() { + final Map data = {}; + data['PatientID'] = patientID; + data['LineItemNo'] = lineItemNo; + data['WeightMeasured'] = weightMeasured; + data['WeightDate'] = weightDate; + data['Remark'] = remark; + data['IsActive'] = isActive; + data['Measured'] = measured; + data['Unit'] = unit; + data['ChartYear'] = chartYear; + data['ChartMonth'] = chartMonth; + data['YearAverageResult'] = yearAverageResult; + data['MonthAverageResult'] = monthAverageResult; + data['WeekAverageResult'] = weekAverageResult; + data['WeekDesc'] = weekDesc; + return data; + } +} diff --git a/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart b/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart new file mode 100644 index 0000000..7dc4b24 --- /dev/null +++ b/lib/features/health_trackers/models/weight/year_weight_measurement_result_average.dart @@ -0,0 +1,29 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class YearWeightMeasurementResultAverage { + dynamic monthAverageResult; + int? monthNumber; + String? monthName; + String? yearName; + DateTime? date; + + YearWeightMeasurementResultAverage({this.monthAverageResult, this.monthNumber, this.monthName, this.yearName}); + + YearWeightMeasurementResultAverage.fromJson(Map json) { + monthAverageResult = json['monthAverageResult']; + monthNumber = json['monthNumber']; + monthName = json['monthName']; + yearName = json['yearName']; + date = DateUtil.getMonthDateTime(monthName!, yearName); + } + + Map toJson() { + final Map data = {}; + data['monthAverageResult'] = monthAverageResult; + data['monthNumber'] = monthNumber; + data['monthName'] = monthName; + data['yearName'] = yearName; + return data; + } +} + diff --git a/lib/features/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart index 4103569..85e6018 100644 --- a/lib/features/hmg_services/hmg_services_repo.dart +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -14,12 +14,14 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; -import 'package:provider/provider.dart'; import 'models/req_models/create_e_referral_model.dart'; import 'models/req_models/send_activation_code_ereferral_req_model.dart'; +import 'models/resq_models/covid_get_test_proceedure_resp.dart'; +import 'models/resq_models/get_covid_payment_info_resp.dart'; import 'models/resq_models/relationship_type_resp_mode.dart'; import 'models/resq_models/search_e_referral_resp_model.dart'; +import 'models/resq_models/vital_sign_respo_model.dart'; abstract class HmgServicesRepo { Future>>> getAllComprehensiveCheckupOrders(); @@ -61,7 +63,11 @@ abstract class HmgServicesRepo { Future>>> searchEReferral(SearchEReferralRequestModel requestModel); + Future>>> getCovidTestProcedures(); + Future>> getCovidPaymentInfo(String procedureID, int projectID); + + Future>>> getPatientVitalSign(); } class HmgServicesRepoImp implements HmgServicesRepo { @@ -817,4 +823,149 @@ class HmgServicesRepoImp implements HmgServicesRepo { } } + + @override + Future>>> getCovidTestProcedures() async { + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + GET_COVID_DRIVETHRU_PROCEDURES_LIST, + body: {"TestTypeEnum":2,"TestProcedureEnum":3,}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Covid Test Procedure : $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List covidTestProcedure = []; + + if (response['COVID19_TestProceduresList'] != null && response['COVID19_TestProceduresList'] is List) { + final servicesList = response['COVID19_TestProceduresList'] as List; + + for (var serviceJson in servicesList) { + if (serviceJson is Map) { + covidTestProcedure.add(Covid19GetTestProceduresResp.fromJson(serviceJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: covidTestProcedure, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in Search Referral: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getCovidPaymentInfo(String procedureID, int projectID) async { + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + GET_COVID_DRIVETHRU_PAYMENT_INFO, + body: {"TestTypeEnum":2,"TestProcedureEnum":3, "ProcedureId":procedureID, "ProjectID":projectID,}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Covid Test Procedure : $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + Covid19GetPaymentInfo covidPaymentInfo = Covid19GetPaymentInfo.fromJson(response["COVID19_PatientShare"]); + + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: covidPaymentInfo, + ); + } catch (e) { + loggerService.logError("Error parsing E-Referral services: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in Search Referral: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getPatientVitalSign() async { + Map requestBody = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + GET_PATIENT_VITAL_SIGN, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + loggerService.logError("Patient Vital Sign API Failed: $error, Status: $statusCode"); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + List vitalSignList = []; + + if (response['List_DoctorPatientVitalSign'] != null && response['List_DoctorPatientVitalSign'] is List) { + final vitalSignsList = response['List_DoctorPatientVitalSign'] as List; + + for (var vitalSignJson in vitalSignsList) { + if (vitalSignJson is Map) { + vitalSignList.add(VitalSignResModel.fromJson(vitalSignJson)); + } + } + } + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: vitalSignList, + ); + } catch (e) { + loggerService.logError("Error parsing Patient Vital Sign: ${e.toString()}"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + log("Unknown error in getPatientVitalSign: ${e.toString()}"); + return Left(UnknownFailure(e.toString())); + } + } + + } diff --git a/lib/features/hmg_services/hmg_services_view_model.dart b/lib/features/hmg_services/hmg_services_view_model.dart index 81cd060..c55a11c 100644 --- a/lib/features/hmg_services/hmg_services_view_model.dart +++ b/lib/features/hmg_services/hmg_services_view_model.dart @@ -1,36 +1,44 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_repo.dart'; -import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/cmc_create_new_order_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/order_update_req_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/search_e_referral_req_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/send_activation_code_ereferral_req_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_services_resp_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/search_e_referral_resp_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'models/req_models/check_activation_e_referral_req_model.dart'; +import 'models/resq_models/get_covid_payment_info_resp.dart'; import 'models/resq_models/relationship_type_resp_mode.dart'; +import 'models/ui_models/covid_questionnare_model.dart'; class HmgServicesViewModel extends ChangeNotifier { final HmgServicesRepo hmgServicesRepo; final BookAppointmentsRepo bookAppointmentsRepo; final ErrorHandlerService errorHandlerService; final NavigationService navigationService; - HmgServicesViewModel({required this.bookAppointmentsRepo, required this.hmgServicesRepo, required this.errorHandlerService, required this.navigationService}); + HmgServicesViewModel( + {required this.bookAppointmentsRepo, required this.hmgServicesRepo, required this.errorHandlerService, required this.navigationService}); bool isCmcOrdersLoading = false; bool isCmcServicesLoading = false; bool isUpdatingOrder = false; bool isHospitalListLoading = false; + bool isVitalSignLoading = false; // HHC specific loading states bool isHhcOrdersLoading = false; @@ -41,6 +49,20 @@ class HmgServicesViewModel extends ChangeNotifier { List hospitalsList = []; List filteredHospitalsList = []; HospitalsModel? selectedHospital; + List vitalSignList = []; + + // Vital Sign PageView Controller + PageController _vitalSignPageController = PageController(); + PageController get vitalSignPageController => _vitalSignPageController; + + int _vitalSignCurrentPage = 0; + int get vitalSignCurrentPage => _vitalSignCurrentPage; + + void setVitalSignCurrentPage(int page) { + _vitalSignCurrentPage = page; + notifyListeners(); + } + // HHC specific lists List hhcOrdersList = []; @@ -53,12 +75,15 @@ class HmgServicesViewModel extends ChangeNotifier { // HHC order creation state (no hospital selection needed for home healthcare) GetCMCServicesResponseModel? selectedServiceForHhcOrder; + List relationTypes = []; + List getAllCitiesList = []; + List searchReferralList = []; + List covidTestProcedureList = []; + Covid19GetPaymentInfo? covidPaymentInfo; + Future getOrdersList() async {} + - List relationTypes =[]; - List getAllCitiesList =[]; - List searchReferralList =[]; - Future getOrdersList() async {} // HHC multiple services selection List selectedHhcServices = []; @@ -332,10 +357,7 @@ class HmgServicesViewModel extends ChangeNotifier { await getAllHhcOrders(); } - Future getAllHhcOrders({ - Function(dynamic)? onSuccess, - Function(String)? onError, - }) async { + Future getAllHhcOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { isHhcOrdersLoading = true; notifyListeners(); @@ -541,16 +563,14 @@ class HmgServicesViewModel extends ChangeNotifier { final result = await hmgServicesRepo.getRelationshipTypes(); result.fold( - (failure) async { - + (failure) async { notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } }, - (apiResponse) { - + (apiResponse) { if (apiResponse.messageStatus == 1) { relationTypes = apiResponse.data ?? []; notifyListeners(); @@ -567,7 +587,6 @@ class HmgServicesViewModel extends ChangeNotifier { ); } - Future getAllCities({ Function(dynamic)? onSuccess, Function(String)? onError, @@ -576,16 +595,14 @@ class HmgServicesViewModel extends ChangeNotifier { final result = await hmgServicesRepo.getAllCities(); result.fold( - (failure) async { - + (failure) async { notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } }, - (apiResponse) { - + (apiResponse) { if (apiResponse.messageStatus == 1) { getAllCitiesList = apiResponse.data ?? []; notifyListeners(); @@ -607,22 +624,20 @@ class HmgServicesViewModel extends ChangeNotifier { Function(GenericApiModel)? onSuccess, Function(String)? onError, }) async { - notifyListeners(); final result = await hmgServicesRepo.sendEReferralActivationCode(requestModel); result.fold( - (failure) async { + (failure) async { notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } }, - (apiResponse) { + (apiResponse) { if (apiResponse.messageStatus == 1) { - notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -642,22 +657,20 @@ class HmgServicesViewModel extends ChangeNotifier { Function(GenericApiModel)? onSuccess, Function(String)? onError, }) async { - notifyListeners(); final result = await hmgServicesRepo.checkEReferralActivationCode(requestModel); result.fold( - (failure) async { + (failure) async { notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } }, - (apiResponse) { + (apiResponse) { if (apiResponse.messageStatus == 1) { - notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -672,28 +685,25 @@ class HmgServicesViewModel extends ChangeNotifier { ); } - Future createEReferral({ required CreateEReferralRequestModel requestModel, Function(GenericApiModel)? onSuccess, Function(String)? onError, }) async { - notifyListeners(); final result = await hmgServicesRepo.createEReferral(requestModel); result.fold( - (failure) async { + (failure) async { notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } }, - (apiResponse) { + (apiResponse) { if (apiResponse.messageStatus == 1) { - notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); @@ -708,29 +718,24 @@ class HmgServicesViewModel extends ChangeNotifier { ); } - - Future searchEReferral({ required SearchEReferralRequestModel requestModel, Function(dynamic)? onSuccess, Function(String)? onError, }) async { - notifyListeners(); final result = await hmgServicesRepo.searchEReferral(requestModel); result.fold( - (failure) async { - + (failure) async { notifyListeners(); await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } }, - (apiResponse) { - + (apiResponse) { if (apiResponse.messageStatus == 1) { searchReferralList = apiResponse.data ?? []; notifyListeners(); @@ -747,28 +752,26 @@ class HmgServicesViewModel extends ChangeNotifier { ); } - Future navigateToOTPScreen( - {required OTPTypeEnum otpTypeEnum, - required String phoneNumber, - required String loginToken, - required Function onSuccess, - }) async { - + Future navigateToOTPScreen({ + required OTPTypeEnum otpTypeEnum, + required String phoneNumber, + required String loginToken, + required Function onSuccess, + }) async { navigationService.pushToOtpScreen( phoneNumber: phoneNumber, isFormFamilyFile: false, checkActivationCode: (int activationCode) async { - checkEReferralActivationCode( requestModel: CheckActivationCodeForEReferralRequestModel( logInTokenID: loginToken, activationCode: activationCode.toString(), ), onSuccess: (GenericApiModel response) { - onSuccess(); + onSuccess(); }, onError: (String errorMessage) { - print(errorMessage); + print(errorMessage); }, ); }, @@ -787,5 +790,129 @@ class HmgServicesViewModel extends ChangeNotifier { }, ); } + List getQuestionsFromJson() { + final String questionsJson = '''[ { "id": 1, "questionEN": "Is the test intended for travel?", "questionAR": "هل تجري التحليل بغرض السفر؟", "ans": 2 }, { "id": 2, "questionEN": "Coming from outside KSA within last 2 weeks?", "questionAR": "هل قدمت من خارج المملكة خلال الأسبوعين الماضيين؟", "ans": 2 }, { "id": 3, "questionEN": "Do you currently have fever?", "questionAR": "هل تعاني حاليا من حرارة؟", "ans": 2 }, { "id": 4, "questionEN": "Did you have fever in last 2 weeks?", "questionAR": "هل عانيت من حرارة في الأسبوعين الماضيين؟", "ans": 2 }, { "id": 5, "questionEN": "Do you have a sore throat?", "questionAR": "هل لديك التهاب في الحلق؟", "ans": 2 }, { "id": 6, "questionEN": "Do you have a runny nose?", "questionAR": "هل لديك سيلان بالأنف؟" }, { "id": 7, "questionEN": "Do you have a cough?", "questionAR": "هل لديك سعال؟", "ans": 2 }, { "id": 8, "questionEN": "Do you have shortness of breath?", "questionAR": "هل تعاني من ضيق في التنفس؟", "ans": 2 }, { "id": 9, "questionEN": "Do you have nausea?", "questionAR": "هل تعاني من غثيان؟", "ans": 2 }, { "id": 10, "questionEN": "Do you have vomiting?", "questionAR": "هل تعاني من القيء؟", "ans": 2 }, { "id": 11, "questionEN": "Do you have a headache?", "questionAR": "هل تعاني من صداع في الرأس؟", "ans": 2 }, { "id": 12, "questionEN": "Do you have muscle pain?", "questionAR": "هل تعانين من آلام عضلية؟", "ans": 2 }, { "id": 13, "questionEN": "Do you have joint pain?", "questionAR": "هل تعاني من آلام المفاصل؟", "ans": 2 }, { "id": 14, "questionEN": "Do you have diarrhea?", "questionAR": "هل لديك اسهال؟", "ans": 2 } ]'''; + + try { + final parsed = json.decode(questionsJson) as List; + return parsed + .map((e) => CovidQuestionnaireModel.fromJson(Map.from(e))) + .toList(); + } catch (_) { + return []; + } + } + + + Future getCovidProcedureList({ + + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.getCovidTestProcedures(); + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + covidTestProcedureList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + + Future getPaymentInfo({ + String? procedureID, + int? projectID, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + notifyListeners(); + + final result = await hmgServicesRepo.getCovidPaymentInfo(procedureID!, projectID!); + + result.fold( + (failure) async { + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 1) { + covidPaymentInfo = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + Future getPatientVitalSign({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isVitalSignLoading = true; + notifyListeners(); + + final result = await hmgServicesRepo.getPatientVitalSign(); + + result.fold( + (failure) async { + isVitalSignLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isVitalSignLoading = false; + if (apiResponse.messageStatus == 1) { + vitalSignList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Unknown error'); + } + } + }, + ); + } + + @override + void dispose() { + _vitalSignPageController.dispose(); + super.dispose(); + } } diff --git a/lib/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart b/lib/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart new file mode 100644 index 0000000..c27f1e7 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +class Covid19GetTestProceduresResp { + String? procedureId; + String? procedureName; + String? procedureNameN; + String? setupId; + + Covid19GetTestProceduresResp({ + this.procedureId, + this.procedureName, + this.procedureNameN, + this.setupId, + }); + + factory Covid19GetTestProceduresResp.fromRawJson(String str) => Covid19GetTestProceduresResp.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Covid19GetTestProceduresResp.fromJson(Map json) => Covid19GetTestProceduresResp( + procedureId: json["ProcedureID"], + procedureName: json["ProcedureName"], + procedureNameN: json["ProcedureNameN"], + setupId: json["SetupID"], + ); + + Map toJson() => { + "ProcedureID": procedureId, + "ProcedureName": procedureName, + "ProcedureNameN": procedureNameN, + "SetupID": setupId, + }; +} diff --git a/lib/features/hmg_services/models/resq_models/get_covid_payment_info_resp.dart b/lib/features/hmg_services/models/resq_models/get_covid_payment_info_resp.dart new file mode 100644 index 0000000..5f95263 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/get_covid_payment_info_resp.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +class Covid19GetPaymentInfo { + dynamic propertyChanged; + int? cashPriceField; + int? cashPriceTaxField; + int? cashPriceWithTaxField; + int? companyIdField; + String? companyNameField; + int? companyShareWithTaxField; + dynamic errCodeField; + int? groupIdField; + dynamic insurancePolicyNoField; + String? messageField; + dynamic patientCardIdField; + int? patientShareField; + double? patientShareWithTaxField; + double? patientTaxAmountField; + int? policyIdField; + dynamic policyNameField; + dynamic procedureIdField; + String? procedureNameField; + dynamic setupIdField; + int? statusCodeField; + dynamic subPolicyNoField; + + Covid19GetPaymentInfo({ + this.propertyChanged, + this.cashPriceField, + this.cashPriceTaxField, + this.cashPriceWithTaxField, + this.companyIdField, + this.companyNameField, + this.companyShareWithTaxField, + this.errCodeField, + this.groupIdField, + this.insurancePolicyNoField, + this.messageField, + this.patientCardIdField, + this.patientShareField, + this.patientShareWithTaxField, + this.patientTaxAmountField, + this.policyIdField, + this.policyNameField, + this.procedureIdField, + this.procedureNameField, + this.setupIdField, + this.statusCodeField, + this.subPolicyNoField, + }); + + factory Covid19GetPaymentInfo.fromRawJson(String str) => Covid19GetPaymentInfo.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Covid19GetPaymentInfo.fromJson(Map json) => Covid19GetPaymentInfo( + propertyChanged: json["PropertyChanged"], + cashPriceField: json["cashPriceField"], + cashPriceTaxField: json["cashPriceTaxField"], + cashPriceWithTaxField: json["cashPriceWithTaxField"], + companyIdField: json["companyIdField"], + companyNameField: json["companyNameField"], + companyShareWithTaxField: json["companyShareWithTaxField"], + errCodeField: json["errCodeField"], + groupIdField: json["groupIDField"], + insurancePolicyNoField: json["insurancePolicyNoField"], + messageField: json["messageField"], + patientCardIdField: json["patientCardIDField"], + patientShareField: json["patientShareField"], + patientShareWithTaxField: json["patientShareWithTaxField"]?.toDouble(), + patientTaxAmountField: json["patientTaxAmountField"]?.toDouble(), + policyIdField: json["policyIdField"], + policyNameField: json["policyNameField"], + procedureIdField: json["procedureIdField"], + procedureNameField: json["procedureNameField"], + setupIdField: json["setupIDField"], + statusCodeField: json["statusCodeField"], + subPolicyNoField: json["subPolicyNoField"], + ); + + Map toJson() => { + "PropertyChanged": propertyChanged, + "cashPriceField": cashPriceField, + "cashPriceTaxField": cashPriceTaxField, + "cashPriceWithTaxField": cashPriceWithTaxField, + "companyIdField": companyIdField, + "companyNameField": companyNameField, + "companyShareWithTaxField": companyShareWithTaxField, + "errCodeField": errCodeField, + "groupIDField": groupIdField, + "insurancePolicyNoField": insurancePolicyNoField, + "messageField": messageField, + "patientCardIDField": patientCardIdField, + "patientShareField": patientShareField, + "patientShareWithTaxField": patientShareWithTaxField, + "patientTaxAmountField": patientTaxAmountField, + "policyIdField": policyIdField, + "policyNameField": policyNameField, + "procedureIdField": procedureIdField, + "procedureNameField": procedureNameField, + "setupIDField": setupIdField, + "statusCodeField": statusCodeField, + "subPolicyNoField": subPolicyNoField, + }; +} diff --git a/lib/features/hmg_services/models/resq_models/vital_sign_respo_model.dart b/lib/features/hmg_services/models/resq_models/vital_sign_respo_model.dart new file mode 100644 index 0000000..bc93f59 --- /dev/null +++ b/lib/features/hmg_services/models/resq_models/vital_sign_respo_model.dart @@ -0,0 +1,259 @@ +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; + +class VitalSignResModel { + var transNo; + var projectID; + var weightKg; + var heightCm; + var temperatureCelcius; + var pulseBeatPerMinute; + var respirationBeatPerMinute; + var bloodPressureLower; + var bloodPressureHigher; + var sAO2; + var fIO2; + var painScore; + var bodyMassIndex; + var headCircumCm; + var leanBodyWeightLbs; + var idealBodyWeightLbs; + var temperatureCelciusMethod; + var pulseRhythm; + var respirationPattern; + var bloodPressureCuffLocation; + var bloodPressureCuffSize; + var bloodPressurePatientPosition; + var painLocation; + var painDuration; + var painCharacter; + var painFrequency; + bool? isPainManagementDone; + var status; + bool? isVitalsRequired; + var patientID; + var createdOn; + var doctorID; + var clinicID; + var triageCategory; + var gCScore; + var lineItemNo; + DateTime? vitalSignDate; + var actualTimeTaken; + var sugarLevel; + var fBS; + var rBS; + var observationType; + var heartRate; + var muscleTone; + var reflexIrritability; + var bodyColor; + var isFirstAssessment; + var dateofBirth; + var timeOfBirth; + var bloodPressure; + var bloodPressureCuffLocationDesc; + var bloodPressureCuffSizeDesc; + var bloodPressurePatientPositionDesc; + var clinicName; + var doctorImageURL; + var doctorName; + var painScoreDesc; + var pulseRhythmDesc; + var respirationPatternDesc; + var temperatureCelciusMethodDesc; + var time; + + VitalSignResModel( + {this.transNo, + this.projectID, + this.weightKg, + this.heightCm, + this.temperatureCelcius, + this.pulseBeatPerMinute, + this.respirationBeatPerMinute, + this.bloodPressureLower, + this.bloodPressureHigher, + this.sAO2, + this.fIO2, + this.painScore, + this.bodyMassIndex, + this.headCircumCm, + this.leanBodyWeightLbs, + this.idealBodyWeightLbs, + this.temperatureCelciusMethod, + this.pulseRhythm, + this.respirationPattern, + this.bloodPressureCuffLocation, + this.bloodPressureCuffSize, + this.bloodPressurePatientPosition, + this.painLocation, + this.painDuration, + this.painCharacter, + this.painFrequency, + this.isPainManagementDone, + this.status, + this.isVitalsRequired, + this.patientID, + this.createdOn, + this.doctorID, + this.clinicID, + this.triageCategory, + this.gCScore, + this.lineItemNo, + this.vitalSignDate, + this.actualTimeTaken, + this.sugarLevel, + this.fBS, + this.rBS, + this.observationType, + this.heartRate, + this.muscleTone, + this.reflexIrritability, + this.bodyColor, + this.isFirstAssessment, + this.dateofBirth, + this.timeOfBirth, + this.bloodPressure, + this.bloodPressureCuffLocationDesc, + this.bloodPressureCuffSizeDesc, + this.bloodPressurePatientPositionDesc, + this.clinicName, + this.doctorImageURL, + this.doctorName, + this.painScoreDesc, + this.pulseRhythmDesc, + this.respirationPatternDesc, + this.temperatureCelciusMethodDesc, + this.time}); + + VitalSignResModel.fromJson(Map json) { + transNo = json['TransNo']; + projectID = json['ProjectID']; + weightKg = json['WeightKg']; + heightCm = json['HeightCm']; + temperatureCelcius = json['TemperatureCelcius']; + pulseBeatPerMinute = json['PulseBeatPerMinute']; + respirationBeatPerMinute = json['RespirationBeatPerMinute']; + bloodPressureLower = json['BloodPressureLower']; + bloodPressureHigher = json['BloodPressureHigher']; + sAO2 = json['SAO2']; + fIO2 = json['FIO2']; + painScore = json['PainScore']; + bodyMassIndex = json['BodyMassIndex']; + headCircumCm = json['HeadCircumCm']; + leanBodyWeightLbs = json['LeanBodyWeightLbs']; + idealBodyWeightLbs = json['IdealBodyWeightLbs']; + temperatureCelciusMethod = json['TemperatureCelciusMethod']; + pulseRhythm = json['PulseRhythm']; + respirationPattern = json['RespirationPattern']; + bloodPressureCuffLocation = json['BloodPressureCuffLocation']; + bloodPressureCuffSize = json['BloodPressureCuffSize']; + bloodPressurePatientPosition = json['BloodPressurePatientPosition']; + painLocation = json['PainLocation']; + painDuration = json['PainDuration']; + painCharacter = json['PainCharacter']; + painFrequency = json['PainFrequency']; + isPainManagementDone = json['IsPainManagementDone']; + status = json['Status']; + isVitalsRequired = json['IsVitalsRequired']; + patientID = json['PatientID']; + createdOn = json['CreatedOn']; + doctorID = json['DoctorID']; + clinicID = json['ClinicID']; + triageCategory = json['TriageCategory']; + gCScore = json['GCScore']; + lineItemNo = json['LineItemNo']; + vitalSignDate = DateUtil.convertStringToDate(json['CreatedOn']); + actualTimeTaken = json['ActualTimeTaken']; + sugarLevel = json['SugarLevel']; + fBS = json['FBS']; + rBS = json['RBS']; + observationType = json['ObservationType']; + heartRate = json['HeartRate']; + muscleTone = json['MuscleTone']; + reflexIrritability = json['ReflexIrritability']; + bodyColor = json['BodyColor']; + isFirstAssessment = json['IsFirstAssessment']; + dateofBirth = json['DateofBirth']; + timeOfBirth = json['TimeOfBirth']; + bloodPressure = json['BloodPressure']; + bloodPressureCuffLocationDesc = json['BloodPressureCuffLocationDesc']; + bloodPressureCuffSizeDesc = json['BloodPressureCuffSizeDesc']; + bloodPressurePatientPositionDesc = json['BloodPressurePatientPositionDesc']; + clinicName = json['ClinicName']; + doctorImageURL = json['DoctorImageURL']; + doctorName = json['DoctorName']; + painScoreDesc = json['PainScoreDesc']; + pulseRhythmDesc = json['PulseRhythmDesc']; + respirationPatternDesc = json['RespirationPatternDesc']; + temperatureCelciusMethodDesc = json['TemperatureCelciusMethodDesc']; + time = json['Time']; + } + + Map toJson() { + final Map data = new Map(); + data['TransNo'] = this.transNo; + data['ProjectID'] = this.projectID; + data['WeightKg'] = this.weightKg; + data['HeightCm'] = this.heightCm; + data['TemperatureCelcius'] = this.temperatureCelcius; + data['PulseBeatPerMinute'] = this.pulseBeatPerMinute; + data['RespirationBeatPerMinute'] = this.respirationBeatPerMinute; + data['BloodPressureLower'] = this.bloodPressureLower; + data['BloodPressureHigher'] = this.bloodPressureHigher; + data['SAO2'] = this.sAO2; + data['FIO2'] = this.fIO2; + data['PainScore'] = this.painScore; + data['BodyMassIndex'] = this.bodyMassIndex; + data['HeadCircumCm'] = this.headCircumCm; + data['LeanBodyWeightLbs'] = this.leanBodyWeightLbs; + data['IdealBodyWeightLbs'] = this.idealBodyWeightLbs; + data['TemperatureCelciusMethod'] = this.temperatureCelciusMethod; + data['PulseRhythm'] = this.pulseRhythm; + data['RespirationPattern'] = this.respirationPattern; + data['BloodPressureCuffLocation'] = this.bloodPressureCuffLocation; + data['BloodPressureCuffSize'] = this.bloodPressureCuffSize; + data['BloodPressurePatientPosition'] = this.bloodPressurePatientPosition; + data['PainLocation'] = this.painLocation; + data['PainDuration'] = this.painDuration; + data['PainCharacter'] = this.painCharacter; + data['PainFrequency'] = this.painFrequency; + data['IsPainManagementDone'] = this.isPainManagementDone; + data['Status'] = this.status; + data['IsVitalsRequired'] = this.isVitalsRequired; + data['PatientID'] = this.patientID; + data['CreatedOn'] = this.createdOn; + data['DoctorID'] = this.doctorID; + data['ClinicID'] = this.clinicID; + data['TriageCategory'] = this.triageCategory; + data['GCScore'] = this.gCScore; + data['LineItemNo'] = this.lineItemNo; + data['VitalSignDate'] = this.vitalSignDate; + data['ActualTimeTaken'] = this.actualTimeTaken; + data['SugarLevel'] = this.sugarLevel; + data['FBS'] = this.fBS; + data['RBS'] = this.rBS; + data['ObservationType'] = this.observationType; + data['HeartRate'] = this.heartRate; + data['MuscleTone'] = this.muscleTone; + data['ReflexIrritability'] = this.reflexIrritability; + data['BodyColor'] = this.bodyColor; + data['IsFirstAssessment'] = this.isFirstAssessment; + data['DateofBirth'] = this.dateofBirth; + data['TimeOfBirth'] = this.timeOfBirth; + data['BloodPressure'] = this.bloodPressure; + data['BloodPressureCuffLocationDesc'] = this.bloodPressureCuffLocationDesc; + data['BloodPressureCuffSizeDesc'] = this.bloodPressureCuffSizeDesc; + data['BloodPressurePatientPositionDesc'] = + this.bloodPressurePatientPositionDesc; + data['ClinicName'] = this.clinicName; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorName'] = this.doctorName; + data['PainScoreDesc'] = this.painScoreDesc; + data['PulseRhythmDesc'] = this.pulseRhythmDesc; + data['RespirationPatternDesc'] = this.respirationPatternDesc; + data['TemperatureCelciusMethodDesc'] = this.temperatureCelciusMethodDesc; + data['Time'] = this.time; + return data; + } +} diff --git a/lib/features/hmg_services/models/ui_models/covid_questionnare_model.dart b/lib/features/hmg_services/models/ui_models/covid_questionnare_model.dart new file mode 100644 index 0000000..35b0efe --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/covid_questionnare_model.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +class CovidQuestionnaireModel { + int? id; + String? questionEn; + String? questionAr; + int? ans; + + CovidQuestionnaireModel({ + this.id, + this.questionEn, + this.questionAr, + this.ans, + }); + + factory CovidQuestionnaireModel.fromRawJson(String str) => CovidQuestionnaireModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory CovidQuestionnaireModel.fromJson(Map json) => CovidQuestionnaireModel( + id: json["id"], + questionEn: json["questionEN"], + questionAr: json["questionAR"], + ans: json["ans"], + ); + + Map toJson() => { + "id": id, + "questionEN": questionEn, + "questionAR": questionAr, + "ans": ans, + }; +} diff --git a/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart b/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart index 32964b1..8be00d6 100644 --- a/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart +++ b/lib/features/hmg_services/models/ui_models/e_referral_form_model.dart @@ -1,5 +1,4 @@ // models/referral_models.dart -import 'package:hmg_patient_app_new/core/common_models/nationality_country_model.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; diff --git a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart new file mode 100644 index 0000000..45b0ab6 --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// UI-only helper model for Vital Sign cards. +/// +/// Keeps presentation logic (chip colors, icon colors, simple status rules) +/// in one place so it can be reused across multiple pages. +class VitalSignUiModel { + final Color iconBg; + final Color iconFg; + final Color chipBg; + final Color chipFg; + + const VitalSignUiModel({ + required this.iconBg, + required this.iconFg, + required this.chipBg, + required this.chipFg, + }); + + /// Returns a color scheme for a card based on its [status] and [label]. + /// + /// Rules (mirrors existing behavior in Medical File page): + /// - Height is always blue. + /// - High => red scheme. + /// - Low => yellow scheme. + /// - Otherwise => green scheme (Normal). + static VitalSignUiModel scheme({required String? status, required String label}) { + final s = (status ?? '').toLowerCase(); + final l = label.toLowerCase(); + + // Height should always be blue. + if (l.contains('height')) { + return VitalSignUiModel( + iconBg: AppColors.infoColor.withValues(alpha: 0.12), + iconFg: AppColors.infoColor, + chipBg: AppColors.infoColor.withValues(alpha: 0.12), + chipFg: AppColors.infoColor, + ); + } + + if (s.contains('high')) { + return const VitalSignUiModel( + iconBg: AppColors.chipSecondaryLightRedColor, + iconFg: AppColors.primaryRedColor, + chipBg: AppColors.chipSecondaryLightRedColor, + chipFg: AppColors.primaryRedColor, + ); + } + + if (s.contains('low')) { + final Color yellowBg = AppColors.warningColor.withValues(alpha: 0.12); + return VitalSignUiModel( + iconBg: yellowBg, + iconFg: AppColors.warningColor, + chipBg: yellowBg, + chipFg: AppColors.warningColor, + ); + } + + // Normal (green) + final Color greenBg = AppColors.lightGreenColor; + return VitalSignUiModel( + iconBg: greenBg, + iconFg: AppColors.bgGreenColor, + chipBg: greenBg, + chipFg: AppColors.bgGreenColor, + ); + } + + /// Simple, user-friendly classification: + /// - Low: systolic < 90 OR diastolic < 60 + /// - High: systolic >= 140 OR diastolic >= 90 + /// - Normal: otherwise + /// Returns null if values are missing/unparseable. + static String? bloodPressureStatus({dynamic systolic, dynamic diastolic}) { + final int? s = toIntOrNull(systolic); + final int? d = toIntOrNull(diastolic); + if (s == null || d == null) return null; + + if (s < 90 || d < 60) return 'Low'; + if (s >= 140 || d >= 90) return 'High'; + return 'Normal'; + } + + static int? toIntOrNull(dynamic v) { + if (v == null) return null; + if (v is int) return v; + if (v is double) return v.round(); + return int.tryParse(v.toString()); + } + + static String bmiStatus(dynamic bmi) { + if (bmi == null) return 'N/A'; + final double bmiValue = double.tryParse(bmi.toString()) ?? 0; + if (bmiValue < 18.5) return 'Underweight'; + if (bmiValue < 25) return 'Normal'; + if (bmiValue < 30) return 'Overweight'; + return 'High'; + } +} + diff --git a/lib/features/hospital/AppPermission.dart b/lib/features/hospital/AppPermission.dart new file mode 100644 index 0000000..008f571 --- /dev/null +++ b/lib/features/hospital/AppPermission.dart @@ -0,0 +1,27 @@ +import 'package:flutter/cupertino.dart'; +import 'package:permission_handler/permission_handler.dart'; + + +class AppPermission { + static Future askVideoCallPermission(BuildContext context) async { + if (!(await Permission.camera.request().isGranted) || !(await Permission.microphone.request().isGranted)) { + return false; + } + // if (Platform.isAndroid && !(await PlatformBridge.shared().isDrawOverAppsPermissionAllowed())) { + // await _drawOverAppsMessageDialog(context); + // return false; + // } + return true; + } + + static Future askPenguinPermissions() async { + if (!(await Permission.location.request().isGranted) || + !(await Permission.bluetooth.request().isGranted) || + !(await Permission.bluetoothScan.request().isGranted) || + !(await Permission.bluetoothConnect.request().isGranted) || + !(await Permission.activityRecognition.request().isGranted)) { + return false; + } + return true; + } +} diff --git a/lib/features/hospital/hospital_selection_view_model.dart b/lib/features/hospital/hospital_selection_view_model.dart new file mode 100644 index 0000000..dd9531f --- /dev/null +++ b/lib/features/hospital/hospital_selection_view_model.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/penguin_method_channel.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/hospital/AppPermission.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class HospitalSelectionBottomSheetViewModel extends ChangeNotifier { + List displayList = []; + List listOfData = []; + List hmgHospitalList = []; + List hmcHospitalList = []; + FacilitySelection selectedFacility = FacilitySelection.ALL; + int hmcCount = 0; + int hmgCount = 0; + TextEditingController searchController = TextEditingController(); + final AppState appState; + + HospitalSelectionBottomSheetViewModel(this.appState) { + Utils.navigationProjectsList.forEach((element) { + HospitalsModel model = HospitalsModel.fromJson(element); + if (model.isHMC == true) { + hmcHospitalList.add(model); + } else { + hmgHospitalList.add(model); + } + listOfData.add(model); + }); + hmgCount = hmgHospitalList.length; + hmcCount = hmcHospitalList.length; + getDisplayList(); + } + + getDisplayList() { + switch (selectedFacility) { + case FacilitySelection.ALL: + displayList = listOfData; + break; + case FacilitySelection.HMG: + displayList = hmgHospitalList; + break; + case FacilitySelection.HMC: + displayList = hmcHospitalList; + break; + } + notifyListeners(); + } + + searchHospitals(String query) { + if (query.isEmpty) { + getDisplayList(); + return; + } + List sourceList = []; + switch (selectedFacility) { + case FacilitySelection.ALL: + sourceList = listOfData; + break; + case FacilitySelection.HMG: + sourceList = hmgHospitalList; + break; + case FacilitySelection.HMC: + sourceList = hmcHospitalList; + break; + } + displayList = sourceList.where((hospital) => hospital.name != null && hospital.name!.toLowerCase().contains(query.toLowerCase())).toList(); + notifyListeners(); + } + + void clearSearchText() { + searchController.clear(); + } + + void setSelectedFacility(FacilitySelection value) { + selectedFacility = value; + getDisplayList(); + + } + + void openPenguin(HospitalsModel hospital) { + initPenguinSDK(hospital.iD); + } + + initPenguinSDK(int projectID) async { + NavigationClinicDetails data = NavigationClinicDetails(); + data.projectId = projectID.toString(); + final bool permited = await AppPermission.askPenguinPermissions(); + if (!permited) { + Map statuses = await [ + Permission.location, + Permission.bluetooth, + Permission.bluetoothConnect, + Permission.bluetoothScan, + Permission.activityRecognition, + ].request().whenComplete(() { + PenguinMethodChannel().launch("penguin", appState.isArabic() ? "ar" : "en", appState.getAuthenticatedUser()?.patientId?.toString()??"", details: data); + }); + } + } + + +} diff --git a/lib/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 30f4c26..0bbdda2 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -5,7 +5,6 @@ import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patien import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_card_history.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_details_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_update_response_model.dart'; -import 'package:hmg_patient_app_new/features/lab/lab_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class InsuranceViewModel extends ChangeNotifier { diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index cd4065e..118c54a0 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -1,6 +1,5 @@ import 'dart:collection'; import 'dart:core'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -18,7 +17,6 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:intl/intl.dart' show DateFormat; -import 'package:logger/logger.dart'; class LabViewModel extends ChangeNotifier { bool isLabOrdersLoading = false; @@ -82,6 +80,12 @@ class LabViewModel extends ChangeNotifier { double minY = double.infinity; double maxX = double.infinity; + // --- New grouping fields --- + bool isSortByClinic = true; + List> patientLabOrdersByClinic = []; + List> patientLabOrdersByHospital = []; + List> patientLabOrdersViewList = []; + LabViewModel( {required this.labRepo, required this.errorHandlerService, @@ -94,6 +98,9 @@ class LabViewModel extends ChangeNotifier { labOrderTests.clear(); isLabOrdersLoading = true; isLabResultsLoading = true; + patientLabOrdersByClinic.clear(); + patientLabOrdersByHospital.clear(); + patientLabOrdersViewList.clear(); getPatientLabOrders(); // } notifyListeners(); @@ -104,6 +111,12 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + void setIsSortByClinic(bool value) { + isSortByClinic = value; + patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; + notifyListeners(); + } + Future getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { // if (!isLabNeedToLoad) return; @@ -112,6 +125,9 @@ class LabViewModel extends ChangeNotifier { uniqueTests.clear(); labOrderTests.clear(); uniqueTests = {}; + patientLabOrdersByClinic.clear(); + patientLabOrdersByHospital.clear(); + patientLabOrdersViewList.clear(); notifyListeners(); final result = await labRepo.getPatientLabOrders(); @@ -122,7 +138,7 @@ class LabViewModel extends ChangeNotifier { isLabResultsLoading = false; notifyListeners(); }, - // => await errorHandlerService.handleError(failure: failure), + // => await errorHandler_service.handleError(failure: failure), (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); @@ -132,6 +148,23 @@ class LabViewModel extends ChangeNotifier { tempLabOrdersList = apiResponse.data!; isLabOrdersLoading = false; isLabResultsLoading = false; + + // --- Build groups by clinic and by hospital (projectName) --- + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var order in patientLabOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + + patientLabOrdersByClinic = clinicMap.values.toList(); + patientLabOrdersByHospital = hospitalMap.values.toList(); + patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; + filterSuggestions(); getUniqueTestDescription(); isLabNeedToLoad = false; @@ -206,7 +239,6 @@ class LabViewModel extends ChangeNotifier { createdOn: item.createdOn, model: item)) }; - var sortedResult = SplayTreeSet.from(uniqueTests, (a, b) => a.description?[0].toUpperCase().compareTo(b.description?[0] ?? "") ?? -1); uniqueTestsList = uniqueTests.toList(); uniqueTestsList.sort((a, b) { return a.description!.toLowerCase().compareTo(b.description!.toLowerCase()); @@ -291,7 +323,7 @@ class LabViewModel extends ChangeNotifier { PatientLabOrdersResponseModel laborder) async { isLabResultByHospitalLoading = true; notifyListeners(); - mainLabResultsByHospitals.clear; + mainLabResultsByHospitals.clear(); final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); @@ -334,8 +366,7 @@ class LabViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - var sortedResponse = sortByFlagAndValue(apiResponse.data!); - var recentThree = sort(sortedResponse); + var recentThree = sort(apiResponse.data!); mainLabResults = recentThree; @@ -617,19 +648,6 @@ class LabViewModel extends ChangeNotifier { } List sort(List original) { - DateTime? parseVerifiedDate(String? raw) { - if (raw == null) return null; - final regex = RegExp(r'\/Date\((\d+)\)\/'); - final match = regex.firstMatch(raw); - if (match != null) { - final millis = int.tryParse(match.group(1)!); - if (millis != null) { - - return DateTime.fromMillisecondsSinceEpoch(millis); - } - } - return null; - } final copy = List.from(original); copy.sort((a, b) { @@ -649,8 +667,8 @@ class LabViewModel extends ChangeNotifier { final copy = List.from(original); copy.sort((a, b) { - final aDate =a.time; - final bDate = a.time; + final aDate = a.time; + final bDate = b.time; final now = DateTime.now(); if (aDate == now && bDate == now) return 0; if (aDate == now) return 1; diff --git a/lib/features/location/location_view_model.dart b/lib/features/location/location_view_model.dart index c6ea34e..5b0eef6 100644 --- a/lib/features/location/location_view_model.dart +++ b/lib/features/location/location_view_model.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show ChangeNotifier; import 'package:flutter/material.dart'; -import 'package:google_maps_flutter_platform_interface/src/types/camera.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart'; @@ -10,9 +10,6 @@ import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart'; import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:huawei_map/huawei_map.dart' as HMSCameraServices; -import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices; - - import 'PlacePrediction.dart'; @@ -20,12 +17,12 @@ class LocationViewModel extends ChangeNotifier { final LocationRepo locationRepo; final ErrorHandlerService errorHandlerService; - LocationViewModel({required this.locationRepo, required this.errorHandlerService}){ + LocationViewModel({required this.locationRepo, required this.errorHandlerService}) { placeValueInController(); } - List predictions = []; - PlacePrediction? selectedPrediction; + List predictions = []; + PlacePrediction? selectedPrediction; bool isPredictionLoading = false; GeocodeResponse? geocodeResponse; PlaceDetails? placeDetails; @@ -39,12 +36,13 @@ class LocationViewModel extends ChangeNotifier { return HMSCameraServices.CameraPosition(target: HMSCameraServices.LatLng(getIt().userLat, getIt().userLong), zoom: 18); } - GMSMapServices.CameraPosition getGMSLocation() { - return GMSMapServices.CameraPosition(target: GMSMapServices.LatLng(getIt().userLat, getIt().userLong), zoom: 18); + return GMSMapServices.CameraPosition( + target: GMSMapServices.LatLng(getIt().userLat != 0.0 ? getIt().userLat : 24.7248316, getIt().userLong != 0.0 ? getIt().userLong : 46.4928828), + zoom: 18); } - void placeValueInController() async{ + void placeValueInController() async { if (await getIt().isGMSAvailable) { gmsController = Completer(); } else { @@ -54,14 +52,14 @@ class LocationViewModel extends ChangeNotifier { FutureOr getPlacesPrediction(String input) async { predictions = []; - isPredictionLoading= true; + isPredictionLoading = true; final result = await locationRepo.getPlacePredictionsAsInput(input); result.fold( (failure) { errorHandlerService.handleError(failure: failure); }, (apiModel) { - predictions = apiModel.data??[]; + predictions = apiModel.data ?? []; }, ); isPredictionLoading = false; @@ -99,21 +97,20 @@ class LocationViewModel extends ChangeNotifier { handleGMSMapCameraMoved(GMSMapServices.CameraPosition value) { mapCapturedLocation = Location(lat: value.target.latitude, lng: value.target.longitude); - } handleHMSMapCameraMoved(HMSCameraServices.CameraPosition value) { mapCapturedLocation = Location(lat: value.target.lat, lng: value.target.lng); } - handleOnCameraIdle(){ - if(mapCapturedLocation != null) { + handleOnCameraIdle() { + if (mapCapturedLocation != null) { getPlaceEncodedData(mapCapturedLocation!.lat, mapCapturedLocation!.lng); } } void updateSearchQuery(String? value) { - if(value == null || value.isEmpty){ + if (value == null || value.isEmpty) { predictions = []; return; } @@ -123,16 +120,16 @@ class LocationViewModel extends ChangeNotifier { void flushSearchPredictions() { predictions = []; - mapCapturedLocation= null; - placeDetails= null; - geocodeResponse= null; - selectedPrediction= null; + mapCapturedLocation = null; + placeDetails = null; + geocodeResponse = null; + selectedPrediction = null; notifyListeners(); } - FutureOr selectPlacePrediction(PlacePrediction placePrediction) async{ - selectedPrediction= placePrediction; + FutureOr selectPlacePrediction(PlacePrediction placePrediction) async { + selectedPrediction = placePrediction; await getPlaceDetails(placePrediction.placeID); } @@ -141,8 +138,6 @@ class LocationViewModel extends ChangeNotifier { } void moveController(Location location) { - print("moving to location"); - print("gmsController is null or not $gmsController"); if (getIt().isGMSAvailable) { gmsController?.future.then((controller) { controller.animateCamera( @@ -169,4 +164,4 @@ class LocationViewModel extends ChangeNotifier { }); } } -} \ No newline at end of file +} diff --git a/lib/features/medical_file/medical_file_repo.dart b/lib/features/medical_file/medical_file_repo.dart index bf10e9e..09856ca 100644 --- a/lib/features/medical_file/medical_file_repo.dart +++ b/lib/features/medical_file/medical_file_repo.dart @@ -3,7 +3,6 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_allergies_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; @@ -294,14 +293,13 @@ class MedicalFileRepoImp implements MedicalFileRepo { Failure? failure; await apiClient.post( ApiConsts.getAllSharedRecordsByStatus, - body: {if (status != null) "Status": status, "PatientID": patientID}, + body: {if (status != null) "Status": status, "PatientID": patientID.toString()}, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - final list = response['GetAllSharedRecordsByStatusList']; - + final list = response['GetAllSharedRecordsByStatusList'] ?? []; final familyLists = list.map((item) => FamilyFileResponseModelLists.fromJson(item as Map)).toList().cast(); @@ -337,7 +335,7 @@ class MedicalFileRepoImp implements MedicalFileRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - final list = response['GetAllPendingRecordsList']; + final list = response['GetAllPendingRecordsList'] ?? []; final familyLists = list.map((item) => FamilyFileResponseModelLists.fromJson(item as Map)).toList().cast(); apiResponse = GenericApiModel>( diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index de7f067..d67e5de 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:developer'; import 'package:flutter/material.dart'; @@ -36,6 +35,12 @@ class MedicalFileViewModel extends ChangeNotifier { List patientVaccineList = []; List patientSickLeaveList = []; + List patientSickLeavesByClinic = []; + List patientSickLeavesByHospital = []; + List patientSickLeavesViewList = []; + + bool isSickLeavesSortByClinic = true; + List patientAllergiesList = []; List patientMedicalReportList = []; @@ -44,6 +49,12 @@ class MedicalFileViewModel extends ChangeNotifier { List patientMedicalReportReadyList = []; List patientMedicalReportCancelledList = []; + List patientMedicalReportsByClinic = []; + List patientMedicalReportsByHospital = []; + List patientMedicalReportsViewList = []; + + bool isMedicalReportsSortByClinic = true; + List patientMedicalReportAppointmentHistoryList = []; PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment; @@ -90,6 +101,49 @@ class MedicalFileViewModel extends ChangeNotifier { } else if (index == 2) { patientMedicalReportList = patientMedicalReportCancelledList; } + // Re-group by clinic/hospital for the selected tab + _groupMedicalReportsByClinicAndHospital(); + notifyListeners(); + } + + void _groupMedicalReportsByClinicAndHospital() { + patientMedicalReportsByClinic.clear(); + patientMedicalReportsByHospital.clear(); + + for (var element in patientMedicalReportList) { + // Group by clinic + List reportsByClinic = patientMedicalReportsByClinic.where((elementClinic) => elementClinic.filterName == element.clinicDescription).toList(); + + if (reportsByClinic.isNotEmpty) { + patientMedicalReportsByClinic[patientMedicalReportsByClinic.indexOf(reportsByClinic[0])].medicalReportsList!.add(element); + } else { + patientMedicalReportsByClinic.add(MedicalReportList(filterName: element.clinicDescription, medicalReport: element)); + } + + // Group by hospital + List reportsByHospital = patientMedicalReportsByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList(); + + if (reportsByHospital.isNotEmpty) { + patientMedicalReportsByHospital[patientMedicalReportsByHospital.indexOf(reportsByHospital[0])].medicalReportsList!.add(element); + } else { + patientMedicalReportsByHospital.add(MedicalReportList(filterName: element.projectName, medicalReport: element)); + } + } + + if (isMedicalReportsSortByClinic) { + patientMedicalReportsViewList = patientMedicalReportsByClinic; + } else { + patientMedicalReportsViewList = patientMedicalReportsByHospital; + } + } + + setIsMedicalReportsSortByClinic(bool value) { + isMedicalReportsSortByClinic = value; + if (isMedicalReportsSortByClinic) { + patientMedicalReportsViewList = patientMedicalReportsByClinic; + } else { + patientMedicalReportsViewList = patientMedicalReportsByHospital; + } notifyListeners(); } @@ -110,17 +164,35 @@ class MedicalFileViewModel extends ChangeNotifier { setIsPatientSickLeaveListLoading(bool val) { if (val) { patientSickLeaveList.clear(); + patientSickLeavesByClinic.clear(); + patientSickLeavesByHospital.clear(); + patientSickLeavesViewList.clear(); patientSickLeavePDFBase64 = ""; + isSickLeavesSortByClinic = true; } isPatientSickLeaveListLoading = val; notifyListeners(); } + setIsSickLeavesSortByClinic(bool value) { + isSickLeavesSortByClinic = value; + if (isSickLeavesSortByClinic) { + patientSickLeavesViewList = patientSickLeavesByClinic; + } else { + patientSickLeavesViewList = patientSickLeavesByHospital; + } + notifyListeners(); + } + setIsPatientMedicalReportsLoading(bool val) { if (val) { onMedicalReportTabChange(0); patientMedicalReportList.clear(); + patientMedicalReportsByClinic.clear(); + patientMedicalReportsByHospital.clear(); + patientMedicalReportsViewList.clear(); patientMedicalReportPDFBase64 = ""; + isMedicalReportsSortByClinic = true; } isPatientMedicalReportsListLoading = val; notifyListeners(); @@ -214,6 +286,29 @@ class MedicalFileViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientSickLeaveList = apiResponse.data!; isPatientSickLeaveListLoading = false; + + // Group by clinic and hospital + for (var element in patientSickLeaveList) { + // Group by clinic + List sickLeavesByClinic = patientSickLeavesByClinic.where((elementClinic) => elementClinic.filterName == element.clinicName).toList(); + + if (sickLeavesByClinic.isNotEmpty) { + patientSickLeavesByClinic[patientSickLeavesByClinic.indexOf(sickLeavesByClinic[0])].sickLeavesList!.add(element); + } else { + patientSickLeavesByClinic.add(SickLeaveList(filterName: element.clinicName, sickLeaves: element)); + } + + // Group by hospital + List sickLeavesByHospital = patientSickLeavesByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList(); + + if (sickLeavesByHospital.isNotEmpty) { + patientSickLeavesByHospital[patientSickLeavesByHospital.indexOf(sickLeavesByHospital[0])].sickLeavesList!.add(element); + } else { + patientSickLeavesByHospital.add(SickLeaveList(filterName: element.projectName, sickLeaves: element)); + } + } + patientSickLeavesViewList = patientSickLeavesByClinic; + notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/features/medical_file/models/patient_medical_response_model.dart b/lib/features/medical_file/models/patient_medical_response_model.dart index 52785af..3bcf5cd 100644 --- a/lib/features/medical_file/models/patient_medical_response_model.dart +++ b/lib/features/medical_file/models/patient_medical_response_model.dart @@ -190,3 +190,16 @@ class PatientMedicalReportResponseModel { return data; } } + +class MedicalReportList { + String? filterName; + List? medicalReportsList; + + MedicalReportList({this.filterName, PatientMedicalReportResponseModel? medicalReport}) { + medicalReportsList = []; + if (medicalReport != null) { + medicalReportsList!.add(medicalReport); + } + } +} + diff --git a/lib/features/medical_file/models/patient_sickleave_response_model.dart b/lib/features/medical_file/models/patient_sickleave_response_model.dart index 3bf732c..53c886f 100644 --- a/lib/features/medical_file/models/patient_sickleave_response_model.dart +++ b/lib/features/medical_file/models/patient_sickleave_response_model.dart @@ -174,3 +174,16 @@ class PatientSickLeavesResponseModel { return data; } } + +class SickLeaveList { + String? filterName; + List? sickLeavesList; + + SickLeaveList({this.filterName, PatientSickLeavesResponseModel? sickLeaves}) { + sickLeavesList = []; + if (sickLeaves != null) { + sickLeavesList!.add(sickLeaves); + } + } +} + diff --git a/lib/features/monthly_report/monthly_report_repo.dart b/lib/features/monthly_report/monthly_report_repo.dart new file mode 100644 index 0000000..429700c --- /dev/null +++ b/lib/features/monthly_report/monthly_report_repo.dart @@ -0,0 +1,53 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class MonthlyReportRepo { + Future>> updatePatientHealthSummaryReport({required bool rSummaryReport}); +} + +class MonthlyReportRepoImp implements MonthlyReportRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MonthlyReportRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>> updatePatientHealthSummaryReport({required bool rSummaryReport}) async { + Map mapDevice = { + "RSummaryReport": rSummaryReport, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + UPDATE_HEALTH_TERMS, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/monthly_report/monthly_report_view_model.dart b/lib/features/monthly_report/monthly_report_view_model.dart new file mode 100644 index 0000000..348ca92 --- /dev/null +++ b/lib/features/monthly_report/monthly_report_view_model.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class MonthlyReportViewModel extends ChangeNotifier { + MonthlyReportRepo monthlyReportRepo; + ErrorHandlerService errorHandlerService; + + bool isUpdateHealthSummaryLoading = false; + bool isHealthSummaryEnabled = false; + + MonthlyReportViewModel({ + required this.monthlyReportRepo, + required this.errorHandlerService, + }); + + setHealthSummaryEnabled(bool value) { + isHealthSummaryEnabled = value; + notifyListeners(); + } + + Future updatePatientHealthSummaryReport({ + required bool rSummaryReport, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isUpdateHealthSummaryLoading = true; + notifyListeners(); + + final result = await monthlyReportRepo.updatePatientHealthSummaryReport( + rSummaryReport: rSummaryReport, + ); + + result.fold( + (failure) async { + isUpdateHealthSummaryLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isUpdateHealthSummaryLoading = false; + if (apiResponse.messageStatus == 2) { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Unknown error"); + } + } else if (apiResponse.messageStatus == 1) { + // Update the local state on success + isHealthSummaryEnabled = rSummaryReport; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/lib/features/my_appointments/appointment_rating_view_model.dart b/lib/features/my_appointments/appointment_rating_view_model.dart new file mode 100644 index 0000000..6dd41ff --- /dev/null +++ b/lib/features/my_appointments/appointment_rating_view_model.dart @@ -0,0 +1,133 @@ +// dart +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +import 'models/resp_models/rate_appointment_resp_model.dart'; + +class AppointmentRatingViewModel extends ChangeNotifier { + final MyAppointmentsRepo myAppointmentsRepo; + final ErrorHandlerService errorHandlerService; + final AppState appState; + List appointmentRatedList = []; + AppointmentDetails? appointmentDetails; + AppointmentRatingViewModel({ + required this.myAppointmentsRepo, + required this.errorHandlerService, + required this.appState, + }); + + + String title = ""; + String subTitle = ""; + bool isRateClinic = false; + + Future getLastRatingAppointment({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getLastRatingAppointment(); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + appointmentRatedList = apiResponse.data ?? []; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future getAppointmentDetails(int appointmentID, int projectID, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getAppointmentDetails(appointmentID, projectID); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + appointmentDetails = apiResponse.data ?? AppointmentDetails(); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + Future submitDoctorRating( {required int docRate, required String docNote,Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendDoctorRate( + docRate, + appointmentDetails!.appointmentNo!, + appointmentDetails!.projectID!, + appointmentDetails!.doctorID!, + appointmentDetails!.clinicID!, + docNote, + appointmentDetails!.appointmentDate!, + appointmentDetails!.doctorName, + appointmentDetails!.projectName, + appointmentDetails!.clinicName + ); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + + notifyListeners(); + if (onSuccess != null) { + // onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future submitClinicRating( { required int clinicRate, required String clinicNote, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendAppointmentRate( + clinicRate, + appointmentDetails!.appointmentNo!, + appointmentDetails!.projectID!, + appointmentDetails!.doctorID!, + appointmentDetails!.clinicID!, + clinicNote + ); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } else if (apiResponse.messageStatus == 1) { + + notifyListeners(); + if (onSuccess != null) { + // onSuccess(apiResponse.data); + } + } + }, + ); + } + + void setSubTitle(String value) { + this.subTitle = value; + notifyListeners(); + } + + void setTitle(String value) { + this.title = value; + notifyListeners(); + } + void setClinicOrDoctor(bool value){ + this.isRateClinic = value; + notifyListeners(); + } +} diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index 4a0ffab..c5dcaf6 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -1,13 +1,16 @@ import 'package:flutter/foundation.dart' show ChangeNotifier; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart' show AppState; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/laser/laser_appointment.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_doctor_page.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; enum AppointmentViaRegionState { REGION_SELECTION, @@ -31,7 +34,14 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { AppointmentViaRegionState bottomSheetState = AppointmentViaRegionState.REGION_SELECTION; final AppState appState; - + TextEditingController searchController = TextEditingController(); + List? hospitalList; + List? hmgHospitalList; + List? hmcHospitalList; + List? displayList; + FacilitySelection selectedFacility = FacilitySelection.ALL; + int hmgCount = 0; + int hmcCount = 0; RegionBottomSheetType regionBottomSheetType = RegionBottomSheetType.FOR_REGION; AppointmentViaRegionViewmodel({required this.navigationService,required this.appState}); @@ -41,6 +51,35 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { notifyListeners(); } + void setDisplayListAndRegionHospitalList(PatientDoctorAppointmentListByRegion? registeredDoctorMap){ + if(registeredDoctorMap == null) { + return; + } + selectedFacility = FacilitySelection.ALL; + hmcHospitalList = []; + hmgHospitalList = []; + hospitalList = []; + displayList = []; + for(var data in registeredDoctorMap.hmgDoctorList!){ + hmgHospitalList?.add(data); + + } + + for(var data in registeredDoctorMap.hmcDoctorList!){ + hmcHospitalList?.add(data); + + } + + hospitalList!.addAll(hmgHospitalList!); + hospitalList!.addAll(hmcHospitalList!); + + hmcCount = registeredDoctorMap.hmcSize; + hmgCount = registeredDoctorMap.hmgSize; + + getDisplayList(); + + } + void setFacility(String? facility) { selectedFacilityType = facility; notifyListeners(); @@ -72,7 +111,7 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { setSelectedRegionId(null); break; case AppointmentViaRegionState.HOSPITAL_SELECTION: - setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); + setBottomSheetState(AppointmentViaRegionState.REGION_SELECTION); break; default: } @@ -130,4 +169,48 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { ), ); } + + searchHospitals(String query) { + if (query.isEmpty) { + getDisplayList(); + return; + } + List? sourceList; + switch (selectedFacility) { + case FacilitySelection.ALL: + sourceList = hospitalList; + break; + case FacilitySelection.HMG: + sourceList = hmgHospitalList; + break; + case FacilitySelection.HMC: + sourceList = hmcHospitalList; + break; + } + displayList = sourceList?.where((hospital) => hospital.filterName != null && hospital.filterName!.toLowerCase().contains(query.toLowerCase())).toList(); + notifyListeners(); + } + + getDisplayList() { + switch (selectedFacility) { + case FacilitySelection.ALL: + displayList = hospitalList; + break; + case FacilitySelection.HMG: + displayList = hmgHospitalList; + break; + case FacilitySelection.HMC: + displayList = hmcHospitalList; + break; + } + notifyListeners(); + } + + + setSelectedFacility(FacilitySelection selection) { + selectedFacility = selection; + notifyListeners(); + } + + } diff --git a/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart b/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart new file mode 100644 index 0000000..91070f2 --- /dev/null +++ b/lib/features/my_appointments/models/req_model/appointment_rate_req_model.dart @@ -0,0 +1,100 @@ +class AppointmentRate { + int? rate; + int? appointmentNo; + int? projectID; + int? doctorID; + int? clinicID; + String? note; + String? mobileNumber; + int? createdBy; + int? editedBy; + double? versionID; + int? channel; + int? languageID; + String? iPAdress; + String? generalid; + int? patientOutSA; + String? sessionID; + bool? isDentalAllowedBackend; + int? deviceTypeID; + int? patientID; + String? tokenID; + int? patientTypeID; + int? patientType; + + AppointmentRate( + {this.rate, + this.appointmentNo, + this.projectID, + this.doctorID, + this.clinicID, + this.note, + this.mobileNumber, + this.createdBy, + this.editedBy, + this.versionID, + this.channel, + this.languageID, + this.iPAdress, + this.generalid, + this.patientOutSA, + this.sessionID, + this.isDentalAllowedBackend, + this.deviceTypeID, + this.patientID, + this.tokenID, + this.patientTypeID, + this.patientType}); + + AppointmentRate.fromJson(Map json) { + rate = json['Rate']; + appointmentNo = json['AppointmentNo']; + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + clinicID = json['ClinicID']; + note = json['Note']; + mobileNumber = json['MobileNumber']; + createdBy = json['CreatedBy']; + editedBy = json['EditedBy']; + versionID = json['VersionID']; + channel = json['Channel']; + languageID = json['LanguageID']; + iPAdress = json['IPAdress']; + generalid = json['generalid']; + patientOutSA = json['PatientOutSA']; + sessionID = json['SessionID']; + isDentalAllowedBackend = json['isDentalAllowedBackend']; + deviceTypeID = json['DeviceTypeID']; + patientID = json['PatientID']; + tokenID = json['TokenID']; + patientTypeID = json['PatientTypeID']; + patientType = json['PatientType']; + } + + Map toJson() { + final Map data = new Map(); + data['Rate'] = this.rate; + data['AppointmentNo'] = this.appointmentNo; + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['ClinicID'] = this.clinicID; + data['Note'] = this.note; + data['MobileNumber'] = this.mobileNumber; + data['CreatedBy'] = this.createdBy; + data['EditedBy'] = this.editedBy; + data['VersionID'] = this.versionID; + data['Channel'] = this.channel; + data['LanguageID'] = this.languageID; + data['IPAdress'] = this.iPAdress; + data['generalid'] = this.generalid; + data['PatientOutSA'] = this.patientOutSA; + data['SessionID'] = this.sessionID; + data['isDentalAllowedBackend'] = this.isDentalAllowedBackend; + data['DeviceTypeID'] = this.deviceTypeID; + data['PatientID'] = this.patientID; + data['TokenID'] = this.tokenID; + data['PatientTypeID'] = this.patientTypeID; + data['PatientType'] = this.patientType; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart b/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart new file mode 100644 index 0000000..2900bce --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/appointment_details_resp_model.dart @@ -0,0 +1,64 @@ +class AppointmentDetails { + String? setupID; + int? projectID; + int? patientID; + int? appointmentNo; + int? clinicID; + int? doctorID; + dynamic startTime; + dynamic endTime; + dynamic appointmentDate; + dynamic clinicName; + dynamic doctorImageURL; + dynamic doctorName; + dynamic projectName; + + AppointmentDetails( + {this.setupID, + this.projectID, + this.patientID, + this.appointmentNo, + this.clinicID, + this.doctorID, + this.startTime, + this.endTime, + this.appointmentDate, + this.clinicName, + this.doctorImageURL, + this.doctorName, + this.projectName}); + + AppointmentDetails.fromJson(Map json) { + setupID = json['SetupID']; + projectID = json['ProjectID']; + patientID = json['PatientID']; + appointmentNo = json['AppointmentNo']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + startTime = json['StartTime']; + endTime = json['EndTime']; + appointmentDate = json['AppointmentDate']; + clinicName = json['ClinicName']; + doctorImageURL = json['DoctorImageURL']; + doctorName = json['DoctorName']; + projectName = json['ProjectName']; + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['PatientID'] = this.patientID; + data['AppointmentNo'] = this.appointmentNo; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['StartTime'] = this.startTime; + data['EndTime'] = this.endTime; + data['AppointmentDate'] = this.appointmentDate; + data['ClinicName'] = this.clinicName; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorName'] = this.doctorName; + data['ProjectName'] = this.projectName; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart b/lib/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart new file mode 100644 index 0000000..c36b9ec --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart @@ -0,0 +1,40 @@ +class PatientQueueDetails { + int? patientID; + String? patientName; + String? queueNo; + int? callType; + String? roomNo; + String? calledOn; + bool? servingNow; + + PatientQueueDetails( + {this.patientID, + this.patientName, + this.queueNo, + this.callType, + this.roomNo, + this.calledOn, + this.servingNow}); + + PatientQueueDetails.fromJson(Map json) { + patientID = json['patientID']; + patientName = json['patientName']; + queueNo = json['queueNo']; + callType = json['callType']; + roomNo = json['roomNo']; + calledOn = json['calledOn']; + servingNow = json['servingNow']; + } + + Map toJson() { + final Map data = new Map(); + data['patientID'] = this.patientID; + data['patientName'] = this.patientName; + data['queueNo'] = this.queueNo; + data['callType'] = this.callType; + data['roomNo'] = this.roomNo; + data['calledOn'] = this.calledOn; + data['servingNow'] = this.servingNow; + return data; + } +} diff --git a/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart b/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart new file mode 100644 index 0000000..877b7bc --- /dev/null +++ b/lib/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart @@ -0,0 +1,160 @@ +class RateAppointmentRespModel { + String? setupID; + int? projectID; + int? appointmentNo; + String? appointmentDate; + String? appointmentDateN; + int? appointmentType; + String? bookDate; + int? patientType; + int? patientID; + int? clinicID; + int? doctorID; + String? endDate; + String? startTime; + String? endTime; + int? status; + int? visitType; + int? visitFor; + int? patientStatusType; + int? companyID; + int? bookedBy; + String? bookedOn; + int? confirmedBy; + String? confirmedOn; + int? arrivalChangedBy; + String? arrivedOn; + int? editedBy; + String? editedOn; + dynamic doctorName; + String? doctorNameN; + String? statusDesc; + String? statusDescN; + bool? vitalStatus; + dynamic vitalSignAppointmentNo; + int? episodeID; + String? doctorTitle; + bool? isAppoitmentLiveCare; + + RateAppointmentRespModel( + {this.setupID, + this.projectID, + this.appointmentNo, + this.appointmentDate, + this.appointmentDateN, + this.appointmentType, + this.bookDate, + this.patientType, + this.patientID, + this.clinicID, + this.doctorID, + this.endDate, + this.startTime, + this.endTime, + this.status, + this.visitType, + this.visitFor, + this.patientStatusType, + this.companyID, + this.bookedBy, + this.bookedOn, + this.confirmedBy, + this.confirmedOn, + this.arrivalChangedBy, + this.arrivedOn, + this.editedBy, + this.editedOn, + this.doctorName, + this.doctorNameN, + this.statusDesc, + this.statusDescN, + this.vitalStatus, + this.vitalSignAppointmentNo, + this.episodeID, + this.doctorTitle, + this.isAppoitmentLiveCare}); + + RateAppointmentRespModel.fromJson(Map json) { + try { + setupID = json['SetupID']; + projectID = json['ProjectID']; + appointmentNo = json['AppointmentNo']; + appointmentDate = json['AppointmentDate']; + appointmentDateN = json['AppointmentDateN']; + appointmentType = json['AppointmentType']; + bookDate = json['BookDate']; + patientType = json['PatientType']; + patientID = json['PatientID']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + endDate = json['EndDate']; + startTime = json['StartTime']; + endTime = json['EndTime']; + status = json['Status']; + visitType = json['VisitType']; + visitFor = json['VisitFor']; + patientStatusType = json['PatientStatusType']; + companyID = json['CompanyID']; + bookedBy = json['BookedBy']; + bookedOn = json['BookedOn']; + confirmedBy = json['ConfirmedBy']; + confirmedOn = json['ConfirmedOn']; + arrivalChangedBy = json['ArrivalChangedBy']; + arrivedOn = json['ArrivedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + statusDesc = json['StatusDesc']; + statusDescN = json['StatusDescN']; + vitalStatus = json['VitalStatus']; + vitalSignAppointmentNo = json['VitalSignAppointmentNo']; + episodeID = json['EpisodeID']; + doctorTitle = json['DoctorTitle']; + isAppoitmentLiveCare = json['IsAppoitmentLiveCare']; + } catch (e) { + print(e); + } + } + + Map toJson() { + final Map data = new Map(); + data['SetupID'] = this.setupID; + data['ProjectID'] = this.projectID; + data['AppointmentNo'] = this.appointmentNo; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentDateN'] = this.appointmentDateN; + data['AppointmentType'] = this.appointmentType; + data['BookDate'] = this.bookDate; + data['PatientType'] = this.patientType; + data['PatientID'] = this.patientID; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['EndDate'] = this.endDate; + data['StartTime'] = this.startTime; + data['EndTime'] = this.endTime; + data['Status'] = this.status; + data['VisitType'] = this.visitType; + data['VisitFor'] = this.visitFor; + data['PatientStatusType'] = this.patientStatusType; + data['CompanyID'] = this.companyID; + data['BookedBy'] = this.bookedBy; + data['BookedOn'] = this.bookedOn; + data['ConfirmedBy'] = this.confirmedBy; + data['ConfirmedOn'] = this.confirmedOn; + data['ArrivalChangedBy'] = this.arrivalChangedBy; + data['ArrivedOn'] = this.arrivedOn; + data['EditedBy'] = this.editedBy; + data['EditedOn'] = this.editedOn; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['StatusDesc'] = this.statusDesc; + data['StatusDescN'] = this.statusDescN; + data['VitalStatus'] = this.vitalStatus; + data['VitalSignAppointmentNo'] = this.vitalSignAppointmentNo; + data['EpisodeID'] = this.episodeID; + data['DoctorTitle'] = this.doctorTitle; + data['IsAppoitmentLiveCare'] = this.isAppoitmentLiveCare; + return data; + } +} diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 87ec10d..ce9e877 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -8,13 +8,17 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'models/req_model/appointment_rate_req_model.dart'; +import 'models/resp_models/appointment_details_resp_model.dart'; + abstract class MyAppointmentsRepo { Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}); @@ -58,6 +62,17 @@ abstract class MyAppointmentsRepo { Future>> sendAskDocCallRequest( {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, required String requestType, required String remarks, required String userMobileNumber}); + + Future>>> getLastRatingAppointment(); + + Future>> getAppointmentDetails(int appointmentID, int projectID); + + + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note); + + Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName); + + Future>>> getPatientAppointmentQueueDetails({required int appointmentNo, required int patientID}); } class MyAppointmentsRepoImp implements MyAppointmentsRepo { @@ -67,7 +82,8 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { MyAppointmentsRepoImp({required this.loggerService, required this.apiClient}); @override - Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async { + Future>>> getPatientAppointments( + {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async { Map mapDevice = { "IsActiveAppointment": isActiveAppointment, "IsComingFromCOC": false, @@ -176,7 +192,9 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { "AppointmentNo": appointmentNo, "PaymentMethodName": paymentMethodName, "PaymentAmount": payedAmount == 0 ? "0" : payedAmount.toString(), - "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime.now().millisecondsSinceEpoch})/", + "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime + .now() + .millisecondsSinceEpoch})/", "PaymentReferenceNumber": payedAmount == 0 ? "" : paymentReference, "ProjectID": projectID, "PatientID": patientID, @@ -746,4 +764,197 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } -} + + @override + Future>>> getPatientAppointmentQueueDetails({required int appointmentNo, required int patientID}) async { + Map mapDevice = {"appointmentNo": appointmentNo, "patientID": patientID, "apiKey": "EE17D21C7943485D9780223CCE55DCE5"}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post(ApiConsts.QLINE_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['data']; + final queueList = list.map((item) => PatientQueueDetails.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: queueList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice, isExternal: true, isAllowAny: true); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getLastRatingAppointment() async { + Map mapDevice = {}; + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post(IS_LAST_APPOITMENT_RATED, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['IsLastAppoitmentRatedList']; + + final lstRatingAppointmentList = list.map((item) => RateAppointmentRespModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: lstRatingAppointmentList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getAppointmentDetails(int appointmentID, int projectID) async { + Map mapDevice = { + "AppointmentNumber": appointmentID, + "ProjectID": projectID, + }; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(GET_APPOINTMENT_DETAILS_BY_NO, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentDetails']; + + final appointmentDetails = AppointmentDetails.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentDetails, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: mapDevice); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + + @override + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note) async { + + AppointmentRate appointmentRate = AppointmentRate(); + appointmentRate.rate = rate; + appointmentRate.appointmentNo = appointmentNo; + appointmentRate.projectID = projectID; + appointmentRate.doctorID = doctorID; + appointmentRate.clinicID = clinicID; + appointmentRate.note = note; + appointmentRate.createdBy = 2; + appointmentRate.editedBy = 2; + + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(NEW_RATE_APPOINTMENT_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentRated']; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: appointmentRate.toJson()); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName) async { + Map request; + + request = { + "DoctorID": doctorID, + "Rate": rate, + "ClinicID": clinicID, + "ProjectID": projectID, + "AppointmentNo": appointmentNo, + "Note": note, + // "MobileNumber": authenticatedUserObject.user!.mobileNumber, + "AppointmentDate": appoDate, + "DoctorName": docName, + "ProjectName": projectName, + "COCTypeName": 1, + // "PatientName": authenticatedUserObject.user!.firstName! + " " + authenticatedUserObject.user!.lastName!, + // "PatientOutSA": authenticatedUserObject.user!.outSA, + // "PatientTypeID": authenticatedUserObject.user!.patientType, + "ClinicName": clinicName, + // "PatientIdentificationID": authenticatedUserObject.user!.patientIdentificationNo + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(NEW_RATE_DOCTOR_URL, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['AppointmentRated']; + + // final appointmentDetails = AppointmentDetails.fromJson(list); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: request); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + +} \ No newline at end of file diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 71e0312..9bd48ec 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_share_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -37,8 +39,14 @@ class MyAppointmentsViewModel extends ChangeNotifier { DateTime? start = null; DateTime? end = null; + bool isAppointmentQueueDetailsLoading = false; bool isPatientHasQueueAppointment = false; int currentQueueStatus = 0; + List patientQueueDetailsList = []; + late PatientQueueDetails currentPatientQueueDetails; + + + List patientAppointmentsHistoryList = []; List filteredAppointmentList = []; @@ -52,6 +60,12 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientEyeMeasurementsAppointmentsHistoryList = []; + // Grouping by Clinic and Hospital + List patientAppointmentsByClinic = []; + List patientAppointmentsByHospital = []; + List patientAppointmentsViewList = []; + bool isAppointmentsSortByClinic = true; + List askDoctorRequestTypeList = []; PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel; @@ -74,8 +88,47 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsAppointmentsSortByClinic(bool value) { + isAppointmentsSortByClinic = value; + _groupAppointmentsByClinicAndHospital(); + notifyListeners(); + } + + void _groupAppointmentsByClinicAndHospital() { + patientAppointmentsByClinic.clear(); + patientAppointmentsByHospital.clear(); + + for (var element in filteredAppointmentList) { + // Group by clinic + List appointmentsByClinic = patientAppointmentsByClinic.where((elementClinic) => elementClinic.filterName == element.clinicName).toList(); + + if (appointmentsByClinic.isNotEmpty) { + patientAppointmentsByClinic[patientAppointmentsByClinic.indexOf(appointmentsByClinic[0])].patientDoctorAppointmentList!.add(element); + } else { + patientAppointmentsByClinic.add(PatientAppointmentList(filterName: element.clinicName, patientDoctorAppointment: element)); + } + + // Group by hospital + List appointmentsByHospital = patientAppointmentsByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList(); + + if (appointmentsByHospital.isNotEmpty) { + patientAppointmentsByHospital[patientAppointmentsByHospital.indexOf(appointmentsByHospital[0])].patientDoctorAppointmentList!.add(element); + } else { + patientAppointmentsByHospital.add(PatientAppointmentList(filterName: element.projectName, patientDoctorAppointment: element)); + } + } + + if (isAppointmentsSortByClinic) { + patientAppointmentsViewList = patientAppointmentsByClinic; + } else { + patientAppointmentsViewList = patientAppointmentsByHospital; + } + } + initAppointmentsViewModel() { if (isAppointmentDataToBeLoaded) { + // Default view is grouped by clinic on first open. + isAppointmentsSortByClinic = true; patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); @@ -88,7 +141,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { isTamaraDetailsLoading = true; isAppointmentPatientShareLoading = true; isEyeMeasurementsAppointmentsLoading = true; - isPatientHasQueueAppointment = false; + notifyListeners(); } @@ -218,10 +271,21 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); filteredAppointmentList.addAll(patientAppointmentsHistoryList); + // Build grouped list immediately so the UI has data for the default (By Clinic) view. + _groupAppointmentsByClinicAndHospital(); + + if (patientArrivedAppointmentsHistoryList.isNotEmpty) { + if (Utils.isDateToday(DateUtil.convertStringToDate(patientArrivedAppointmentsHistoryList.first.appointmentDate))) { + // getPatientAppointmentQueueDetails(appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID); + getPatientAppointmentQueueDetails(); + } + } + print('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}'); print('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); print('All Appointments: ${patientAppointmentsHistoryList.length}'); getFiltersForSelectedAppointmentList(filteredAppointmentList); + notifyListeners(); } void getFiltersForSelectedAppointmentList(List filteredAppointmentList) { @@ -498,6 +562,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { break; } getFiltersForSelectedAppointmentList(filteredAppointmentList); + _groupAppointmentsByClinicAndHospital(); notifyListeners(); } @@ -555,6 +620,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } catch (e) {} } } + _groupAppointmentsByClinicAndHospital(); notifyListeners(); } @@ -659,6 +725,51 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } + Future getPatientAppointmentQueueDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isAppointmentQueueDetailsLoading = true; + notifyListeners(); + final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails( + appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID); + + isAppointmentQueueDetailsLoading = false; + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + onError!(apiResponse.errorMessage!); + } else if (apiResponse.messageStatus == 1) { + if (apiResponse.data != null && apiResponse.data!.isNotEmpty) { + isPatientHasQueueAppointment = true; + patientQueueDetailsList = apiResponse.data!; + for (var element in patientQueueDetailsList) { + if (element.patientID == patientArrivedAppointmentsHistoryList.first.patientID) { + currentPatientQueueDetails = element; + currentQueueStatus = element.callType!; + // currentQueueStatus = 2; + break; + } + } + // patientQueueDetailsList.first.callType = 1; + + patientQueueDetailsList.removeWhere((element) => element.patientID == patientArrivedAppointmentsHistoryList.first.patientID); + } else { + isPatientHasQueueAppointment = false; + } + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + Future sendAskDocCallRequest({ required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, required String requestType, @@ -688,4 +799,5 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + } diff --git a/lib/features/my_invoices/models/get_invoice_details_response_model.dart b/lib/features/my_invoices/models/get_invoice_details_response_model.dart new file mode 100644 index 0000000..ef88623 --- /dev/null +++ b/lib/features/my_invoices/models/get_invoice_details_response_model.dart @@ -0,0 +1,489 @@ +class GetInvoiceDetailsResponseModel { + int? projectID; + int? doctorID; + num? grandTotal; + num? quantity; + num? total; + num? discount; + num? subTotal; + int? invoiceNo; + String? createdOn; + String? procedureID; + String? procedureName; + String? procedureNameN; + num? procedurePrice; + num? patientShare; + num? companyShare; + num? totalPatientShare; + num? totalCompanyShare; + num? totalShare; + num? discountAmount; + num? vATPercentage; + num? patientVATAmount; + num? companyVATAmount; + num? totalVATAmount; + num? price; + int? patientID; + String? patientIdentificationNo; + String? patientName; + String? patientNameN; + String? nationalityID; + String? doctorName; + String? doctorNameN; + int? clinicID; + String? clinicDescription; + String? clinicDescriptionN; + String? appointmentDate; + int? appointmentNo; + String? insuranceID; + int? companyID; + String? companyName; + String? companyNameN; + String? companyAddress; + String? companyAddressN; + String? companyGroupAddress; + String? groupName; + String? groupNameN; + String? patientAddress; + String? vATNo; + String? paymentDate; + String? projectName; + num? totalDiscount; + num? totalPatientShareWithQuantity; + String? legalName; + String? legalNameN; + num? advanceAdjustment; + String? patientCityName; + String? patientCityNameN; + String? doctorImageURL; + List? listConsultation; + + GetInvoiceDetailsResponseModel( + {this.projectID, + this.doctorID, + this.grandTotal, + this.quantity, + this.total, + this.discount, + this.subTotal, + this.invoiceNo, + this.createdOn, + this.procedureID, + this.procedureName, + this.procedureNameN, + this.procedurePrice, + this.patientShare, + this.companyShare, + this.totalPatientShare, + this.totalCompanyShare, + this.totalShare, + this.discountAmount, + this.vATPercentage, + this.patientVATAmount, + this.companyVATAmount, + this.totalVATAmount, + this.price, + this.patientID, + this.patientIdentificationNo, + this.patientName, + this.patientNameN, + this.nationalityID, + this.doctorName, + this.doctorNameN, + this.clinicID, + this.clinicDescription, + this.clinicDescriptionN, + this.appointmentDate, + this.appointmentNo, + this.insuranceID, + this.companyID, + this.companyName, + this.companyNameN, + this.companyAddress, + this.companyAddressN, + this.companyGroupAddress, + this.groupName, + this.groupNameN, + this.patientAddress, + this.vATNo, + this.paymentDate, + this.projectName, + this.totalDiscount, + this.totalPatientShareWithQuantity, + this.legalName, + this.legalNameN, + this.advanceAdjustment, + this.patientCityName, + this.patientCityNameN, + this.doctorImageURL, + this.listConsultation}); + + GetInvoiceDetailsResponseModel.fromJson(Map json) { + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + grandTotal = json['GrandTotal']; + quantity = json['Quantity']; + total = json['Total']; + discount = json['Discount']; + subTotal = json['SubTotal']; + invoiceNo = json['InvoiceNo']; + createdOn = json['CreatedOn']; + procedureID = json['ProcedureID']; + procedureName = json['ProcedureName']; + procedureNameN = json['ProcedureNameN']; + procedurePrice = json['ProcedurePrice']; + patientShare = json['PatientShare']; + companyShare = json['CompanyShare']; + totalPatientShare = json['TotalPatientShare']; + totalCompanyShare = json['TotalCompanyShare']; + totalShare = json['TotalShare']; + discountAmount = json['DiscountAmount']; + vATPercentage = json['VATPercentage']; + patientVATAmount = json['PatientVATAmount']; + companyVATAmount = json['CompanyVATAmount']; + totalVATAmount = json['TotalVATAmount']; + price = json['Price']; + patientID = json['PatientID']; + patientIdentificationNo = json['PatientIdentificationNo']; + patientName = json['PatientName']; + patientNameN = json['PatientNameN']; + nationalityID = json['NationalityID']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + clinicID = json['ClinicID']; + clinicDescription = json['ClinicDescription']; + clinicDescriptionN = json['ClinicDescriptionN']; + appointmentDate = json['AppointmentDate']; + appointmentNo = json['AppointmentNo']; + insuranceID = json['InsuranceID']; + companyID = json['CompanyID']; + companyName = json['CompanyName']; + companyNameN = json['CompanyNameN']; + companyAddress = json['CompanyAddress']; + companyAddressN = json['CompanyAddressN']; + companyGroupAddress = json['CompanyGroupAddress']; + groupName = json['GroupName']; + groupNameN = json['GroupNameN']; + patientAddress = json['PatientAddress']; + vATNo = json['VATNo']; + paymentDate = json['PaymentDate']; + projectName = json['ProjectName']; + totalDiscount = json['TotalDiscount']; + totalPatientShareWithQuantity = json['TotalPatientShareWithQuantity']; + legalName = json['LegalName']; + legalNameN = json['LegalNameN']; + advanceAdjustment = json['AdvanceAdjustment']; + patientCityName = json['PatientCityName']; + patientCityNameN = json['PatientCityNameN']; + doctorImageURL = json['DoctorImageURL']; + if (json['listConsultation'] != null) { + listConsultation = []; + json['listConsultation'].forEach((v) { + listConsultation!.add(new ListConsultation.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['GrandTotal'] = this.grandTotal; + data['Quantity'] = this.quantity; + data['Total'] = this.total; + data['Discount'] = this.discount; + data['SubTotal'] = this.subTotal; + data['InvoiceNo'] = this.invoiceNo; + data['CreatedOn'] = this.createdOn; + data['ProcedureID'] = this.procedureID; + data['ProcedureName'] = this.procedureName; + data['ProcedureNameN'] = this.procedureNameN; + data['ProcedurePrice'] = this.procedurePrice; + data['PatientShare'] = this.patientShare; + data['CompanyShare'] = this.companyShare; + data['TotalPatientShare'] = this.totalPatientShare; + data['TotalCompanyShare'] = this.totalCompanyShare; + data['TotalShare'] = this.totalShare; + data['DiscountAmount'] = this.discountAmount; + data['VATPercentage'] = this.vATPercentage; + data['PatientVATAmount'] = this.patientVATAmount; + data['CompanyVATAmount'] = this.companyVATAmount; + data['TotalVATAmount'] = this.totalVATAmount; + data['Price'] = this.price; + data['PatientID'] = this.patientID; + data['PatientIdentificationNo'] = this.patientIdentificationNo; + data['PatientName'] = this.patientName; + data['PatientNameN'] = this.patientNameN; + data['NationalityID'] = this.nationalityID; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['ClinicID'] = this.clinicID; + data['ClinicDescription'] = this.clinicDescription; + data['ClinicDescriptionN'] = this.clinicDescriptionN; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentNo'] = this.appointmentNo; + data['InsuranceID'] = this.insuranceID; + data['CompanyID'] = this.companyID; + data['CompanyName'] = this.companyName; + data['CompanyNameN'] = this.companyNameN; + data['CompanyAddress'] = this.companyAddress; + data['CompanyAddressN'] = this.companyAddressN; + data['CompanyGroupAddress'] = this.companyGroupAddress; + data['GroupName'] = this.groupName; + data['GroupNameN'] = this.groupNameN; + data['PatientAddress'] = this.patientAddress; + data['VATNo'] = this.vATNo; + data['PaymentDate'] = this.paymentDate; + data['ProjectName'] = this.projectName; + data['TotalDiscount'] = this.totalDiscount; + data['TotalPatientShareWithQuantity'] = this.totalPatientShareWithQuantity; + data['LegalName'] = this.legalName; + data['LegalNameN'] = this.legalNameN; + data['AdvanceAdjustment'] = this.advanceAdjustment; + data['PatientCityName'] = this.patientCityName; + data['PatientCityNameN'] = this.patientCityNameN; + data['DoctorImageURL'] = this.doctorImageURL; + if (this.listConsultation != null) { + data['listConsultation'] = + this.listConsultation!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class ListConsultation { + int? projectID; + int? doctorID; + num? grandTotal; + int? quantity; + num? total; + num? discount; + num? subTotal; + int? invoiceNo; + String? createdOn; + String? procedureID; + String? procedureName; + String? procedureNameN; + num? procedurePrice; + num? patientShare; + num? companyShare; + num? totalPatientShare; + num? totalCompanyShare; + num? totalShare; + num? discountAmount; + num? vATPercentage; + num? patientVATAmount; + num? companyVATAmount; + num? totalVATAmount; + num? price; + int? patientID; + int? patientIdentificationNo; + String? patientName; + String? patientNameN; + String? nationalityID; + String? doctorName; + String? doctorNameN; + int? clinicID; + String? clinicDescription; + String? clinicDescriptionN; + String? appointmentDate; + dynamic appointmentNo; + dynamic insuranceID; + dynamic companyID; + String? companyName; + String? companyNameN; + String? companyAddress; + String? companyAddressN; + String? companyGroupAddress; + String? groupName; + String? groupNameN; + String? patientAddress; + String? vATNo; + String? paymentDate; + String? projectName; + num? totalDiscount; + num? totalPatientShareWithQuantity; + String? legalName; + String? legalNameN; + num? advanceAdjustment; + String? patientCityName; + String? patientCityNameN; + + ListConsultation( + {this.projectID, + this.doctorID, + this.grandTotal, + this.quantity, + this.total, + this.discount, + this.subTotal, + this.invoiceNo, + this.createdOn, + this.procedureID, + this.procedureName, + this.procedureNameN, + this.procedurePrice, + this.patientShare, + this.companyShare, + this.totalPatientShare, + this.totalCompanyShare, + this.totalShare, + this.discountAmount, + this.vATPercentage, + this.patientVATAmount, + this.companyVATAmount, + this.totalVATAmount, + this.price, + this.patientID, + this.patientIdentificationNo, + this.patientName, + this.patientNameN, + this.nationalityID, + this.doctorName, + this.doctorNameN, + this.clinicID, + this.clinicDescription, + this.clinicDescriptionN, + this.appointmentDate, + this.appointmentNo, + this.insuranceID, + this.companyID, + this.companyName, + this.companyNameN, + this.companyAddress, + this.companyAddressN, + this.companyGroupAddress, + this.groupName, + this.groupNameN, + this.patientAddress, + this.vATNo, + this.paymentDate, + this.projectName, + this.totalDiscount, + this.totalPatientShareWithQuantity, + this.legalName, + this.legalNameN, + this.advanceAdjustment, + this.patientCityName, + this.patientCityNameN}); + + ListConsultation.fromJson(Map json) { + projectID = json['ProjectID']; + doctorID = json['DoctorID']; + grandTotal = json['GrandTotal']; + quantity = json['Quantity']; + total = json['Total']; + discount = json['Discount']; + subTotal = json['SubTotal']; + invoiceNo = json['InvoiceNo']; + createdOn = json['CreatedOn']; + procedureID = json['ProcedureID']; + procedureName = json['ProcedureName']; + procedureNameN = json['ProcedureNameN']; + procedurePrice = json['ProcedurePrice']; + patientShare = json['PatientShare']; + companyShare = json['CompanyShare']; + totalPatientShare = json['TotalPatientShare']; + totalCompanyShare = json['TotalCompanyShare']; + totalShare = json['TotalShare']; + discountAmount = json['DiscountAmount']; + vATPercentage = json['VATPercentage']; + patientVATAmount = json['PatientVATAmount']; + companyVATAmount = json['CompanyVATAmount']; + totalVATAmount = json['TotalVATAmount']; + price = json['Price']; + patientID = json['PatientID']; + patientIdentificationNo = json['PatientIdentificationNo']; + patientName = json['PatientName']; + patientNameN = json['PatientNameN']; + nationalityID = json['NationalityID']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + clinicID = json['ClinicID']; + clinicDescription = json['ClinicDescription']; + clinicDescriptionN = json['ClinicDescriptionN']; + appointmentDate = json['AppointmentDate']; + appointmentNo = json['AppointmentNo']; + insuranceID = json['InsuranceID']; + companyID = json['CompanyID']; + companyName = json['CompanyName']; + companyNameN = json['CompanyNameN']; + companyAddress = json['CompanyAddress']; + companyAddressN = json['CompanyAddressN']; + companyGroupAddress = json['CompanyGroupAddress']; + groupName = json['GroupName']; + groupNameN = json['GroupNameN']; + patientAddress = json['PatientAddress']; + vATNo = json['VATNo']; + paymentDate = json['PaymentDate']; + projectName = json['ProjectName']; + totalDiscount = json['TotalDiscount']; + totalPatientShareWithQuantity = json['TotalPatientShareWithQuantity']; + legalName = json['LegalName']; + legalNameN = json['LegalNameN']; + advanceAdjustment = json['AdvanceAdjustment']; + patientCityName = json['PatientCityName']; + patientCityNameN = json['PatientCityNameN']; + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['DoctorID'] = this.doctorID; + data['GrandTotal'] = this.grandTotal; + data['Quantity'] = this.quantity; + data['Total'] = this.total; + data['Discount'] = this.discount; + data['SubTotal'] = this.subTotal; + data['InvoiceNo'] = this.invoiceNo; + data['CreatedOn'] = this.createdOn; + data['ProcedureID'] = this.procedureID; + data['ProcedureName'] = this.procedureName; + data['ProcedureNameN'] = this.procedureNameN; + data['ProcedurePrice'] = this.procedurePrice; + data['PatientShare'] = this.patientShare; + data['CompanyShare'] = this.companyShare; + data['TotalPatientShare'] = this.totalPatientShare; + data['TotalCompanyShare'] = this.totalCompanyShare; + data['TotalShare'] = this.totalShare; + data['DiscountAmount'] = this.discountAmount; + data['VATPercentage'] = this.vATPercentage; + data['PatientVATAmount'] = this.patientVATAmount; + data['CompanyVATAmount'] = this.companyVATAmount; + data['TotalVATAmount'] = this.totalVATAmount; + data['Price'] = this.price; + data['PatientID'] = this.patientID; + data['PatientIdentificationNo'] = this.patientIdentificationNo; + data['PatientName'] = this.patientName; + data['PatientNameN'] = this.patientNameN; + data['NationalityID'] = this.nationalityID; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['ClinicID'] = this.clinicID; + data['ClinicDescription'] = this.clinicDescription; + data['ClinicDescriptionN'] = this.clinicDescriptionN; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentNo'] = this.appointmentNo; + data['InsuranceID'] = this.insuranceID; + data['CompanyID'] = this.companyID; + data['CompanyName'] = this.companyName; + data['CompanyNameN'] = this.companyNameN; + data['CompanyAddress'] = this.companyAddress; + data['CompanyAddressN'] = this.companyAddressN; + data['CompanyGroupAddress'] = this.companyGroupAddress; + data['GroupName'] = this.groupName; + data['GroupNameN'] = this.groupNameN; + data['PatientAddress'] = this.patientAddress; + data['VATNo'] = this.vATNo; + data['PaymentDate'] = this.paymentDate; + data['ProjectName'] = this.projectName; + data['TotalDiscount'] = this.totalDiscount; + data['TotalPatientShareWithQuantity'] = this.totalPatientShareWithQuantity; + data['LegalName'] = this.legalName; + data['LegalNameN'] = this.legalNameN; + data['AdvanceAdjustment'] = this.advanceAdjustment; + data['PatientCityName'] = this.patientCityName; + data['PatientCityNameN'] = this.patientCityNameN; + return data; + } +} diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart new file mode 100644 index 0000000..e8056d9 --- /dev/null +++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart @@ -0,0 +1,88 @@ +class GetInvoicesListResponseModel { + String? setupId; + int? projectID; + int? patientID; + int? appointmentNo; + String? appointmentDate; + String? appointmentDateN; + int? clinicID; + int? doctorID; + int? invoiceNo; + int? status; + String? arrivedOn; + String? doctorName; + String? doctorNameN; + String? clinicName; + double? decimalDoctorRate; + String? doctorImageURL; + int? doctorRate; + int? patientNumber; + String? projectName; + + GetInvoicesListResponseModel( + {this.setupId, + this.projectID, + this.patientID, + this.appointmentNo, + this.appointmentDate, + this.appointmentDateN, + this.clinicID, + this.doctorID, + this.invoiceNo, + this.status, + this.arrivedOn, + this.doctorName, + this.doctorNameN, + this.clinicName, + this.decimalDoctorRate, + this.doctorImageURL, + this.doctorRate, + this.patientNumber, + this.projectName}); + + GetInvoicesListResponseModel.fromJson(Map json) { + setupId = json['SetupId']; + projectID = json['ProjectID']; + patientID = json['PatientID']; + appointmentNo = json['AppointmentNo']; + appointmentDate = json['AppointmentDate']; + appointmentDateN = json['AppointmentDateN']; + clinicID = json['ClinicID']; + doctorID = json['DoctorID']; + invoiceNo = json['InvoiceNo']; + status = json['Status']; + arrivedOn = json['ArrivedOn']; + doctorName = json['DoctorName']; + doctorNameN = json['DoctorNameN']; + clinicName = json['ClinicName']; + decimalDoctorRate = json['DecimalDoctorRate']; + doctorImageURL = json['DoctorImageURL']; + doctorRate = json['DoctorRate']; + patientNumber = json['PatientNumber']; + projectName = json['ProjectName']; + } + + Map toJson() { + final Map data = {}; + data['SetupId'] = this.setupId; + data['ProjectID'] = this.projectID; + data['PatientID'] = this.patientID; + data['AppointmentNo'] = this.appointmentNo; + data['AppointmentDate'] = this.appointmentDate; + data['AppointmentDateN'] = this.appointmentDateN; + data['ClinicID'] = this.clinicID; + data['DoctorID'] = this.doctorID; + data['InvoiceNo'] = this.invoiceNo; + data['Status'] = this.status; + data['ArrivedOn'] = this.arrivedOn; + data['DoctorName'] = this.doctorName; + data['DoctorNameN'] = this.doctorNameN; + data['ClinicName'] = this.clinicName; + data['DecimalDoctorRate'] = this.decimalDoctorRate; + data['DoctorImageURL'] = this.doctorImageURL; + data['DoctorRate'] = this.doctorRate; + data['PatientNumber'] = this.patientNumber; + data['ProjectName'] = this.projectName; + return data; + } +} diff --git a/lib/features/my_invoices/my_invoices_repo.dart b/lib/features/my_invoices/my_invoices_repo.dart new file mode 100644 index 0000000..68eee6e --- /dev/null +++ b/lib/features/my_invoices/my_invoices_repo.dart @@ -0,0 +1,141 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class MyInvoicesRepo { + Future>>> getAllInvoicesList(); + + Future>> getInvoiceDetails({required num appointmentNo, required num invoiceNo, required int projectID}); + + Future>> sendInvoiceEmail({required num appointmentNo, required int projectID}); +} + +class MyInvoicesRepoImp implements MyInvoicesRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MyInvoicesRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>>> getAllInvoicesList() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_ALL_APPOINTMENTS_FOR_DENTAL_CLINIC, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_DentalAppointments']; + + final invoicesList = list.map((item) => GetInvoicesListResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: invoicesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getInvoiceDetails({required num appointmentNo, required num invoiceNo, required int projectID}) async { + Map mapDevice = { + "AppointmentNo": appointmentNo, + "InvoiceNo": invoiceNo, + "IsRegistered": true, + "ProjectID": projectID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + GET_DENTAL_APPOINTMENT_INVOICE, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['List_eInvoiceForDental']; + final invoicesList = GetInvoiceDetailsResponseModel.fromJson(list[0]); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: invoicesList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future> sendInvoiceEmail({required num appointmentNo, required int projectID}) async { + Map mapDevice = { + "AppointmentNo": appointmentNo, + "IsRegistered": true, + "ProjectID": projectID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + SEND_DENTAL_APPOINTMENT_INVOICE_EMAIL, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/my_invoices/my_invoices_view_model.dart b/lib/features/my_invoices/my_invoices_view_model.dart new file mode 100644 index 0000000..a02d741 --- /dev/null +++ b/lib/features/my_invoices/my_invoices_view_model.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; + +class MyInvoicesViewModel extends ChangeNotifier { + bool isInvoicesListLoading = false; + bool isInvoiceDetailsLoading = false; + + MyInvoicesRepo myInvoicesRepo; + ErrorHandlerService errorHandlerService; + NavigationService navServices; + + List allInvoicesList = []; + late GetInvoiceDetailsResponseModel invoiceDetailsResponseModel; + + MyInvoicesViewModel({required this.myInvoicesRepo, required this.errorHandlerService, required this.navServices}); + + setInvoicesListLoading() { + isInvoicesListLoading = true; + allInvoicesList.clear(); + notifyListeners(); + } + + setInvoiceDetailLoading() { + isInvoiceDetailsLoading = true; + notifyListeners(); + } + + Future getAllInvoicesList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myInvoicesRepo.getAllInvoicesList(); + + result.fold( + (failure) async { + isInvoicesListLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + allInvoicesList = apiResponse.data!; + isInvoicesListLoading = false; + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future getInvoiceDetails({required num appointmentNo, required num invoiceNo, required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myInvoicesRepo.getInvoiceDetails(appointmentNo: appointmentNo, invoiceNo: invoiceNo, projectID: projectID); + + result.fold( + (failure) async { + isInvoiceDetailsLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + invoiceDetailsResponseModel = apiResponse.data!; + isInvoiceDetailsLoading = false; + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future sendInvoiceEmail({required num appointmentNo, required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myInvoicesRepo.sendInvoiceEmail(appointmentNo: appointmentNo, projectID: projectID); + + result.fold( + (failure) async { + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } +} diff --git a/lib/features/payfort/payfort_view_model.dart b/lib/features/payfort/payfort_view_model.dart index db8209a..dda8f51 100644 --- a/lib/features/payfort/payfort_view_model.dart +++ b/lib/features/payfort/payfort_view_model.dart @@ -110,7 +110,7 @@ class PayfortViewModel extends ChangeNotifier { onError!(failure.message); }, (apiResponse) { - log(apiResponse.data); + log(apiResponse.data.toString()); if (onSuccess != null) { onSuccess(apiResponse); } @@ -134,7 +134,7 @@ class PayfortViewModel extends ChangeNotifier { onError!(failure.message); }, (apiResponse) { - log(apiResponse.data); + log(apiResponse.data.toString()); if (onSuccess != null) { onSuccess(apiResponse); } diff --git a/lib/features/prescriptions/prescriptions_repo.dart b/lib/features/prescriptions/prescriptions_repo.dart index e7a4f07..c6e7150 100644 --- a/lib/features/prescriptions/prescriptions_repo.dart +++ b/lib/features/prescriptions/prescriptions_repo.dart @@ -182,6 +182,7 @@ class PrescriptionsRepoImp implements PrescriptionsRepo { "To": Utils.appState.getAuthenticatedUser()!.emailAddress, "SetupID": prescriptionsResponseModel.setupID, "IsDownload": true, + "isDentalAllowedBackend": false, }; try { diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index 7517134..ff86406 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -5,8 +5,6 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart'; -import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart'; -import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; diff --git a/lib/features/radiology/radiology_view_model.dart b/lib/features/radiology/radiology_view_model.dart index 4a87906..986945e 100644 --- a/lib/features/radiology/radiology_view_model.dart +++ b/lib/features/radiology/radiology_view_model.dart @@ -29,6 +29,18 @@ class RadiologyViewModel extends ChangeNotifier { late PatientRadiologyResponseModel patientRadiologyOrderByAppointment; + // --- Grouping fields: By Clinic / By Hospital --- + bool isSortByClinic = true; + List> patientRadiologyOrdersByClinic = []; + List> patientRadiologyOrdersByHospital = []; + List> patientRadiologyOrdersViewList = []; + + void setIsSortByClinic(bool value) { + isSortByClinic = value; + patientRadiologyOrdersViewList = isSortByClinic ? patientRadiologyOrdersByClinic : patientRadiologyOrdersByHospital; + notifyListeners(); + } + RadiologyViewModel({required this.radiologyRepo, required this.errorHandlerService, required this.navigationService}); initRadiologyViewModel() { diff --git a/lib/features/smartwatch_health_data/health_provider.dart b/lib/features/smartwatch_health_data/health_provider.dart new file mode 100644 index 0000000..fc9dacc --- /dev/null +++ b/lib/features/smartwatch_health_data/health_provider.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_service.dart'; + +class HealthProvider with ChangeNotifier { + final HealthService _healthService = HealthService(); + Map> healthData = {}; + bool isLoading = false; + String? error; + String selectedTimeRange = '7D'; + int selectedTabIndex = 0; + + void onTabChanged(int index) { + selectedTabIndex = index; + notifyListeners(); + } + + Future fetchHealthData() async { + isLoading = true; + error = null; + notifyListeners(); + healthData.clear(); + try { + final authorized = await _healthService.requestAuthorization(); + if (!authorized) { + error = 'Health data access not authorized'; + isLoading = false; + notifyListeners(); + return; + } + + final startTime = _getStartDate(); + final endTime = DateTime.now(); + + healthData = await _healthService.getAllHealthData(startTime, endTime); + + isLoading = false; + notifyListeners(); + } catch (e) { + error = 'Error fetching health data: $e'; + isLoading = false; + notifyListeners(); + } + } + + Future refreshMetric(HealthDataType type) async { + try { + final startTime = _getStartDate(); + final endTime = DateTime.now(); + + final data = await _healthService.getSpecificHealthData( + type, + startTime, + endTime, + ); + + healthData[type] = data; + notifyListeners(); + } catch (e) { + print('Error refreshing metric $type: $e'); + } + } + + void updateTimeRange(String range) { + selectedTimeRange = range; + fetchHealthData(); + } + + DateTime _getStartDate() { + switch (selectedTimeRange) { + case '1D': + return DateTime.now().subtract(const Duration(days: 1)); + case '7D': + return DateTime.now().subtract(const Duration(days: 7)); + case '1M': + return DateTime.now().subtract(const Duration(days: 30)); + case '3M': + return DateTime.now().subtract(const Duration(days: 90)); + case '1Y': + return DateTime.now().subtract(const Duration(days: 365)); + default: + return DateTime.now().subtract(const Duration(days: 7)); + } + } +} diff --git a/lib/features/smartwatch_health_data/health_service.dart b/lib/features/smartwatch_health_data/health_service.dart new file mode 100644 index 0000000..d3815b3 --- /dev/null +++ b/lib/features/smartwatch_health_data/health_service.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:health/health.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'health_utils.dart'; + +class HealthService { + static final HealthService _instance = HealthService._internal(); + + factory HealthService() => _instance; + + HealthService._internal(); + + final Health health = Health(); + + final List _healthMetrics = [ + HealthDataType.HEART_RATE, + // HealthDataType.STEPS, + // HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BODY_TEMPERATURE, + // HealthDataType.DISTANCE_WALKING_RUNNING, + // HealthDataType.ACTIVE_ENERGY_BURNED, + ]; + + final List _healthMetricsAndroid = [ + HealthDataType.HEART_RATE, + HealthDataType.STEPS, + HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BODY_TEMPERATURE, + HealthDataType.DISTANCE_DELTA, + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + final List _healthPermissions = [ + HealthDataType.HEART_RATE, + HealthDataType.STEPS, + HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BODY_TEMPERATURE, + Platform.isAndroid ? HealthDataType.DISTANCE_DELTA : HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + final List _healthMetricsCumulative = [ + HealthDataType.BLOOD_OXYGEN, + HealthDataType.STEPS, + Platform.isAndroid ? HealthDataType.DISTANCE_DELTA : HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + Future requestAuthorization() async { + try { + final types = (Platform.isAndroid) ? dataTypesAndroid : _healthPermissions; + final granted = await health.requestAuthorization(types); + await Permission.activityRecognition.request(); + await Permission.location.request(); + // request access to read historic data + await Health().requestHealthDataHistoryAuthorization(); + // await authorize(); + return granted; + } catch (e) { + print('Authorization error: $e'); + return false; + } + } + + Future>> getAllHealthData( + DateTime startTime, + DateTime endTime, + ) async { + final Map> allData = {}; + + try { + for (var type in _healthMetricsCumulative) { + try { + final data = await health.getHealthIntervalDataFromTypes( + startDate: startTime, + endDate: endTime, + types: [type], + interval: 86400, + ); + + if (type == HealthDataType.BLOOD_OXYGEN) { + for (var point in data) { + if (point.value is NumericHealthValue) { + final numericValue = (point.value as NumericHealthValue).numericValue; + point.value = NumericHealthValue( + numericValue: numericValue * 100, + ); + } + } + } + + if (type == HealthDataType.DISTANCE_WALKING_RUNNING) { + for (var point in data) { + if (point.value is NumericHealthValue) { + final numericValue = (point.value as NumericHealthValue).numericValue; + point.value = NumericHealthValue( + numericValue: numericValue / 1000, + ); + } + } + } + + allData[type] = data; + } catch (e) { + print('Error fetching $type: $e'); + allData[type] = []; + } + } + + for (var type in Platform.isIOS ? _healthMetrics : _healthMetricsAndroid) { + try { + final data = await health.getHealthDataFromTypes( + startTime: startTime, + endTime: endTime, + types: [type], + // includeManualEntry: false + // interval: 86400, + ); + + allData[type] = data; + } catch (e) { + print('Error fetching $type: $e'); + allData[type] = []; + } + } + } catch (e) { + print('Error fetching health data: $e'); + } + + return allData; + } + + Future> getSpecificHealthData( + HealthDataType type, + DateTime startTime, + DateTime endTime, + ) async { + try { + final data = await health.getHealthDataFromTypes( + startTime: startTime, + endTime: endTime, + types: [type], + ); + return data; + } catch (e) { + print('Error fetching $type: $e'); + return []; + } + } +} diff --git a/lib/features/smartwatch_health_data/health_utils.dart b/lib/features/smartwatch_health_data/health_utils.dart new file mode 100644 index 0000000..3d3588e --- /dev/null +++ b/lib/features/smartwatch_health_data/health_utils.dart @@ -0,0 +1,109 @@ +import 'package:health/health.dart'; + +/// Data types available on iOS via Apple Health. +const List dataTypesIOS = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.APPLE_STAND_TIME, + HealthDataType.APPLE_STAND_HOUR, + HealthDataType.APPLE_MOVE_TIME, + HealthDataType.AUDIOGRAM, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.BLOOD_GLUCOSE, + HealthDataType.BLOOD_OXYGEN, + HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.BODY_MASS_INDEX, + HealthDataType.BODY_TEMPERATURE, + HealthDataType.DIETARY_CARBS_CONSUMED, + HealthDataType.DIETARY_CAFFEINE, + HealthDataType.DIETARY_ENERGY_CONSUMED, + HealthDataType.DIETARY_FATS_CONSUMED, + HealthDataType.DIETARY_PROTEIN_CONSUMED, + HealthDataType.ELECTRODERMAL_ACTIVITY, + HealthDataType.FORCED_EXPIRATORY_VOLUME, + HealthDataType.HEART_RATE, + HealthDataType.HEART_RATE_VARIABILITY_SDNN, + HealthDataType.HEIGHT, + HealthDataType.INSULIN_DELIVERY, + HealthDataType.RESPIRATORY_RATE, + HealthDataType.PERIPHERAL_PERFUSION_INDEX, + HealthDataType.STEPS, + HealthDataType.WAIST_CIRCUMFERENCE, + HealthDataType.WEIGHT, + HealthDataType.FLIGHTS_CLIMBED, + HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.WALKING_SPEED, + HealthDataType.MINDFULNESS, + HealthDataType.SLEEP_AWAKE, + HealthDataType.SLEEP_ASLEEP, + HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_LIGHT, + HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_REM, + HealthDataType.WATER, + HealthDataType.EXERCISE_TIME, + HealthDataType.WORKOUT, + HealthDataType.HEADACHE_NOT_PRESENT, + HealthDataType.HEADACHE_MILD, + HealthDataType.HEADACHE_MODERATE, + HealthDataType.HEADACHE_SEVERE, + HealthDataType.HEADACHE_UNSPECIFIED, + HealthDataType.LEAN_BODY_MASS, + + // note that a phone cannot write these ECG-based types - only read them + // HealthDataType.ELECTROCARDIOGRAM, + // HealthDataType.HIGH_HEART_RATE_EVENT, + // HealthDataType.IRREGULAR_HEART_RATE_EVENT, + // HealthDataType.LOW_HEART_RATE_EVENT, + // HealthDataType.RESTING_HEART_RATE, + // HealthDataType.WALKING_HEART_RATE, + // HealthDataType.ATRIAL_FIBRILLATION_BURDEN, + + HealthDataType.NUTRITION, + HealthDataType.GENDER, + HealthDataType.BLOOD_TYPE, + HealthDataType.BIRTH_DATE, + HealthDataType.MENSTRUATION_FLOW, + HealthDataType.WATER_TEMPERATURE, + HealthDataType.UNDERWATER_DEPTH, + HealthDataType.UV_INDEX, +]; + +/// Data types available on Android via the Google Health Connect API. +const List dataTypesAndroid = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + // HealthDataType.BASAL_ENERGY_BURNED, + // HealthDataType.BLOOD_GLUCOSE, + HealthDataType.BLOOD_OXYGEN, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // HealthDataType.BODY_FAT_PERCENTAGE, + // HealthDataType.HEIGHT, + // HealthDataType.WEIGHT, + // HealthDataType.LEAN_BODY_MASS, + // HealthDataType.BODY_MASS_INDEX, + // HealthDataType.BODY_TEMPERATURE, + HealthDataType.HEART_RATE, + // HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + HealthDataType.STEPS, + HealthDataType.DISTANCE_DELTA, + // HealthDataType.SPEED, + HealthDataType.RESPIRATORY_RATE, + // HealthDataType.SLEEP_ASLEEP, + // HealthDataType.SLEEP_AWAKE_IN_BED, + // HealthDataType.SLEEP_AWAKE, + // HealthDataType.SLEEP_DEEP, + // HealthDataType.SLEEP_LIGHT, + // HealthDataType.SLEEP_OUT_OF_BED, + // HealthDataType.SLEEP_REM, + // HealthDataType.SLEEP_UNKNOWN, + // HealthDataType.SLEEP_SESSION, + // HealthDataType.WATER, + // HealthDataType.WORKOUT, + HealthDataType.RESTING_HEART_RATE, + // HealthDataType.FLIGHTS_CLIMBED, + // HealthDataType.NUTRITION, + HealthDataType.TOTAL_CALORIES_BURNED, + // HealthDataType.MENSTRUATION_FLOW, +]; \ No newline at end of file diff --git a/lib/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart b/lib/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart new file mode 100644 index 0000000..b2be4a2 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart @@ -0,0 +1,59 @@ +class RiskAndSuggestionsResponseModel { + final List? dataDetails; + + RiskAndSuggestionsResponseModel({this.dataDetails}); + + factory RiskAndSuggestionsResponseModel.fromJson(Map json) { + return RiskAndSuggestionsResponseModel( + dataDetails: + json['dataDetails'] != null ? (json['dataDetails'] as List).map((item) => RiskAndSuggestionsItemModel.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'dataDetails': dataDetails?.map((item) => item.toJson()).toList(), + }; + } +} + +class RiskAndSuggestionsItemModel { + final String? id; + final String? type; + final String? name; + final String? commonName; + final String? language; + + RiskAndSuggestionsItemModel({ + this.id, + this.type, + this.name, + this.commonName, + this.language, + }); + + factory RiskAndSuggestionsItemModel.fromJson(Map json) { + return RiskAndSuggestionsItemModel( + id: json['id'], + type: json['type'], + name: json['name'], + commonName: json['common_name'], + language: json['language'], + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + 'name': name, + 'common_name': commonName, + 'language': language, + }; + } + + // Helper method to get display name + String getDisplayName() { + return commonName ?? name ?? ''; + } +} diff --git a/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart new file mode 100644 index 0000000..c0466c4 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart @@ -0,0 +1,188 @@ +class SymptomsUserDetailsResponseModel { + final TokenDetails? tokenDetails; + final UserDetails? userDetails; + final String? sessionId; + + SymptomsUserDetailsResponseModel({ + this.tokenDetails, + this.userDetails, + this.sessionId, + }); + + factory SymptomsUserDetailsResponseModel.fromJson(Map json) { + return SymptomsUserDetailsResponseModel( + tokenDetails: json['tokenDetails'] != null ? TokenDetails.fromJson(json['tokenDetails']) : null, + userDetails: json['userDetails'] != null ? UserDetails.fromJson(json['userDetails']) : null, + sessionId: json['sessionId'], + ); + } + + Map toJson() { + return { + 'tokenDetails': tokenDetails?.toJson(), + 'userDetails': userDetails?.toJson(), + 'sessionId': sessionId, + }; + } +} + +class TokenDetails { + final String? id; + final String? authToken; + final int? expiresIn; + + TokenDetails({ + this.id, + this.authToken, + this.expiresIn, + }); + + factory TokenDetails.fromJson(Map json) { + return TokenDetails( + id: json['id'], + authToken: json['auth_token'], + expiresIn: json['expires_in'], + ); + } + + Map toJson() { + return { + 'id': id, + 'auth_token': authToken, + 'expires_in': expiresIn, + }; + } +} + +class UserDetails { + final String? fileNo; + final String? nationalId; + final String? email; + final String? dateOfBirth; + final String? dateOfBirthHijri; + final int? age; + final UserName? name; + final int? maritalStatus; + final String? maritalStatusCode; + final String? nationality; + final String? nationalityIsoCode; + final String? occupation; + final int? idType; + final int? gender; + final String? jwtToken; + final String? countryDialCode; + final String? phoneNo; + + UserDetails({ + this.fileNo, + this.nationalId, + this.email, + this.dateOfBirth, + this.dateOfBirthHijri, + this.age, + this.name, + this.maritalStatus, + this.maritalStatusCode, + this.nationality, + this.nationalityIsoCode, + this.occupation, + this.idType, + this.gender, + this.jwtToken, + this.countryDialCode, + this.phoneNo, + }); + + factory UserDetails.fromJson(Map json) { + return UserDetails( + fileNo: json['FileNo'], + nationalId: json['national_id'], + email: json['email'], + dateOfBirth: json['date_of_birth'], + dateOfBirthHijri: json['date_of_birth_hijri'], + age: json['age'], + name: json['name'] != null ? UserName.fromJson(json['name']) : null, + maritalStatus: json['marital_status'], + maritalStatusCode: json['marital_status_code'], + nationality: json['nationality'], + nationalityIsoCode: json['nationality_iso_code'], + occupation: json['occupation'], + idType: json['id_type'], + gender: json['gender'], + jwtToken: json['jwt_token'], + countryDialCode: json['country_dial_code'], + phoneNo: json['phone_no'], + ); + } + + Map toJson() { + return { + 'FileNo': fileNo, + 'national_id': nationalId, + 'email': email, + 'date_of_birth': dateOfBirth, + 'date_of_birth_hijri': dateOfBirthHijri, + 'age': age, + 'name': name?.toJson(), + 'marital_status': maritalStatus, + 'marital_status_code': maritalStatusCode, + 'nationality': nationality, + 'nationality_iso_code': nationalityIsoCode, + 'occupation': occupation, + 'id_type': idType, + 'gender': gender, + 'jwt_token': jwtToken, + 'country_dial_code': countryDialCode, + 'phone_no': phoneNo, + }; + } + + // Helper method to get full name + String getFullName(bool isArabic) { + if (name == null) return ''; + if (isArabic) { + return '${name!.firstNameAr ?? ''} ${name!.middleNameAr ?? ''} ${name!.lastNameAr ?? ''}'.trim(); + } + return '${name!.firstName ?? ''} ${name!.middleName ?? ''} ${name!.lastName ?? ''}'.trim(); + } +} + +class UserName { + final String? firstName; + final String? middleName; + final String? lastName; + final String? firstNameAr; + final String? middleNameAr; + final String? lastNameAr; + + UserName({ + this.firstName, + this.middleName, + this.lastName, + this.firstNameAr, + this.middleNameAr, + this.lastNameAr, + }); + + factory UserName.fromJson(Map json) { + return UserName( + firstName: json['first_name'], + middleName: json['middle_name'], + lastName: json['last_name'], + firstNameAr: json['first_name_ar'], + middleNameAr: json['middle_name_ar'], + lastNameAr: json['last_name_ar'], + ); + } + + Map toJson() { + return { + 'first_name': firstName, + 'middle_name': middleName, + 'last_name': lastName, + 'first_name_ar': firstNameAr, + 'middle_name_ar': middleNameAr, + 'last_name_ar': lastNameAr, + }; + } +} diff --git a/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart new file mode 100644 index 0000000..f0d7e90 --- /dev/null +++ b/lib/features/symptoms_checker/models/resp_models/triage_response_model.dart @@ -0,0 +1,209 @@ +class TriageDataDetails { + final TriageQuestion? question; + final List? conditions; + final bool? hasEmergencyEvidence; + final bool? shouldStop; + final String? interviewToken; + final String? message; + final List? errorList; + final int? id; + final String? language; + final String? generalId; + final String? createDate; + final String? lastEditDate; + final String? createdBy; + final String? lastEditBy; + final bool? active; + final int? sortOrder; + final int? userType; + final String? userId; + + TriageDataDetails({ + this.question, + this.conditions, + this.hasEmergencyEvidence, + this.shouldStop, + this.interviewToken, + this.message, + this.errorList, + this.id, + this.language, + this.generalId, + this.createDate, + this.lastEditDate, + this.createdBy, + this.lastEditBy, + this.active, + this.sortOrder, + this.userType, + this.userId, + }); + + factory TriageDataDetails.fromJson(Map json) { + return TriageDataDetails( + question: json['question'] != null ? TriageQuestion.fromJson(json['question']) : null, + conditions: json['conditions'] != null ? (json['conditions'] as List).map((item) => TriageCondition.fromJson(item)).toList() : null, + hasEmergencyEvidence: json['has_emergency_evidence'], + shouldStop: json['should_stop'], + interviewToken: json['interview_token'], + message: json['Message'], + errorList: json['ErrorList'] != null ? List.from(json['ErrorList']) : null, + id: json['Id'], + language: json['language'], + generalId: json['generalId'], + createDate: json['CreateDate'], + lastEditDate: json['LastEditDate'], + createdBy: json['CreatedBy'], + lastEditBy: json['LastEditBy'], + active: json['Active'], + sortOrder: json['SortOrder'], + userType: json['userType'], + userId: json['userId'], + ); + } + + Map toJson() { + return { + 'question': question?.toJson(), + 'conditions': conditions?.map((item) => item.toJson()).toList(), + 'has_emergency_evidence': hasEmergencyEvidence, + 'should_stop': shouldStop, + 'interview_token': interviewToken, + 'Message': message, + 'ErrorList': errorList, + 'Id': id, + 'language': language, + 'generalId': generalId, + 'CreateDate': createDate, + 'LastEditDate': lastEditDate, + 'CreatedBy': createdBy, + 'LastEditBy': lastEditBy, + 'Active': active, + 'SortOrder': sortOrder, + 'userType': userType, + 'userId': userId, + }; + } +} + +class TriageQuestion { + final int? type; + final String? text; + final List? items; + + TriageQuestion({ + this.type, + this.text, + this.items, + }); + + factory TriageQuestion.fromJson(Map json) { + return TriageQuestion( + type: json['type'], + text: json['text'], + items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'type': type, + 'text': text, + 'items': items?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageQuestionItem { + final String? id; + final String? name; + final List? choices; + + TriageQuestionItem({ + this.id, + this.name, + this.choices, + }); + + factory TriageQuestionItem.fromJson(Map json) { + return TriageQuestionItem( + id: json['id'], + name: json['name'], + choices: json['choices'] != null ? (json['choices'] as List).map((item) => TriageChoice.fromJson(item)).toList() : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'choices': choices?.map((item) => item.toJson()).toList(), + }; + } +} + +class TriageChoice { + final String? id; + final String? label; + + TriageChoice({ + this.id, + this.label, + }); + + factory TriageChoice.fromJson(Map json) { + return TriageChoice( + id: json['id'], + label: json['label'], + ); + } + + Map toJson() { + return { + 'id': id, + 'label': label, + }; + } +} + +class TriageCondition { + final String? id; + final String? name; + final String? commonName; + final double? probability; + final dynamic conditionDetails; + + TriageCondition({ + this.id, + this.name, + this.commonName, + this.probability, + this.conditionDetails, + }); + + factory TriageCondition.fromJson(Map json) { + return TriageCondition( + id: json['id'], + name: json['name'], + commonName: json['common_name'], + probability: json['probability']?.toDouble(), + conditionDetails: json['condition_details'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'common_name': commonName, + 'probability': probability, + 'condition_details': conditionDetails, + }; + } + + /// Get probability as percentage + String getProbabilityPercentage() { + if (probability == null) return '0%'; + return '${(probability! * 100).toStringAsFixed(1)}%'; + } +} diff --git a/lib/features/symptoms_checker/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 5379207..c55c2f0 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; @@ -7,12 +6,57 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; -import 'package:http/http.dart' as http; abstract class SymptomsCheckerRepo { - Future>> getBodySymptomsByName({ + Future>> + getUserDetails({ + required String userName, + required String password, + }); + + Future>> + getBodySymptomsByName({ required List organNames, + required String userSessionToken, + required int gender, + }); + + Future>> + getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, + }); + + Future>> + getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required String sessionId, + required int gender, + }); + + Future>> + getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + List>? triageEvidence, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, }); } @@ -20,66 +64,357 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo { final ApiClient apiClient; final LoggerService loggerService; - SymptomsCheckerRepoImp({ - required this.apiClient, - required this.loggerService, - }); + SymptomsCheckerRepoImp( + {required this.apiClient, required this.loggerService}); + + @override + Future>> + getUserDetails({ + required String userName, + required String password, + }) async { + Map body = {"userName": userName, "password": password}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.symptomsUserLogin, + body: body, + isExternal: true, + isAllowAny: true, + isBodyPlainText: false, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("getUserDetails API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = + response is String ? jsonDecode(response) : response; + + SymptomsUserDetailsResponseModel symptomsUserDetailsResponseModel = + SymptomsUserDetailsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: symptomsUserDetailsResponseModel, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing getUserDetails response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getUserDetails: $e"); + loggerService.logError("StackTrace: $stackTrace"); + return Left(UnknownFailure(e.toString())); + } + } @override - Future>> getBodySymptomsByName({ + Future>> + getBodySymptomsByName({ required List organNames, + required String userSessionToken, + required int gender, + }) async { + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + Map body = { + 'bodyPartName': organNames, + 'gender': gender, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBodySymptomsByName, + apiHeaders: headers, + body: body, + isExternal: true, + isAllowAny: true, + isBodyPlainText: false, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + BodySymptomResponseModel bodySymptomResponse = + BodySymptomResponseModel.fromJson(response); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: bodySymptomResponse, + ); + } catch (e, stackTrace) { + loggerService + .logError("Error parsing GetBodySymptomsByName response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getBodySymptomsByName: $e"); + loggerService.logError("StackTrace: $stackTrace"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> + getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required int gender, + required String sessionId, }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRiskFactors, + apiHeaders: headers, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("GetRiskFactors API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = + response is String ? jsonDecode(response) : response; + + RiskAndSuggestionsResponseModel riskFactorsResponse = + RiskAndSuggestionsResponseModel.fromJson(responseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing GetRiskFactors response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getRiskFactors: $e"); + loggerService.logError("StackTrace: $stackTrace"); + return Left(UnknownFailure(e.toString())); + } + } + + Future>> + getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + List>? + triageEvidence, // Additional triage-specific evidence + required String language, + required String userSessionToken, + required int gender, + required String sessionId, + }) async { + // Build evidence list: combine initial symptoms with triage evidence + List> evidenceList = []; + + // Add initial evidence as simple IDs + for (var id in evidenceIds) { + evidenceList.add({"id": id}); + } + + // Add triage evidence with proper format (id, choice_id, source) + if (triageEvidence != null && triageEvidence.isNotEmpty) { + evidenceList.addAll(triageEvidence); + } + + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceList, + "language": language, + "suggest_method": "diagnosis", + "generalId": sessionId, + }; + + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; + try { - // API expects a direct JSON array: ["mid_abdomen", "chest"] - // Not an object like: {"organNames": [...]} - // Since ApiClient.post expects Map and encodes it as object, - // we make direct HTTP call here to send array body - - final String requestBody = jsonEncode(organNames); - - loggerService.logInfo("GetBodySymptomsByName Request: $requestBody"); - log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}"); - log("GetBodySymptomsByName Request Body: $requestBody"); - - // Make direct HTTP POST request with JSON array body - final response = await http.post( - Uri.parse(ApiConsts.getBodySymptomsByName), - headers: {'Content-Type': 'application/json', 'Accept': 'text/plain'}, - body: requestBody, + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.diagnosis, + apiHeaders: headers, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("getDiagnosisForTriage API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = + response is String ? jsonDecode(response) : response; + + final updatedResponseData = responseData['dataDetails']; + + TriageDataDetails riskFactorsResponse = + TriageDataDetails.fromJson(updatedResponseData); + + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService + .logError("Error parsing getDiagnosisForTriage response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, ); - final int statusCode = response.statusCode; + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e, stackTrace) { + loggerService.logError("Exception in getDiagnosisForTriage: $e"); + loggerService.logError("StackTrace: $stackTrace"); + return Left(UnknownFailure(e.toString())); + } + } - log("GetBodySymptomsByName Response Status: $statusCode"); - loggerService.logInfo("GetBodySymptomsByName Response Status: $statusCode"); + @override + Future>> + getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + required String userSessionToken, + required String sessionId, + required int gender, + }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + "generalId": sessionId, + }; - try { - // Parse the response - final responseBody = jsonDecode(response.body); + Map headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $userSessionToken', + }; - loggerService.logInfo("GetBodySymptomsByName API Success: $responseBody"); - log("GetBodySymptomsByName Response: $responseBody"); + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getSuggestions, + apiHeaders: headers, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("GetSuggestions API Failed: $error"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Parse response if it's a string + final Map responseData = + response is String ? jsonDecode(response) : response; - BodySymptomResponseModel bodySymptomResponse = BodySymptomResponseModel.fromJson(responseBody); + RiskAndSuggestionsResponseModel riskFactorsResponse = + RiskAndSuggestionsResponseModel.fromJson(responseData); - GenericApiModel apiResponse = GenericApiModel( - messageStatus: 1, - statusCode: statusCode, - errorMessage: null, - data: bodySymptomResponse, - ); + apiResponse = GenericApiModel( + messageStatus: messageStatus ?? 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: riskFactorsResponse, + ); + } catch (e, stackTrace) { + loggerService.logError("Error parsing GetSuggestions response: $e"); + loggerService.logError("StackTrace: $stackTrace"); + failure = DataParsingFailure(e.toString()); + } + }, + ); - return Right(apiResponse); - } catch (e, stackTrace) { - loggerService.logError("Error parsing GetBodySymptomsByName response: $e"); - loggerService.logError("StackTrace: $stackTrace"); - log("Parse Error: $e"); - return Left(DataParsingFailure(e.toString())); - } + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); } catch (e, stackTrace) { - loggerService.logError("Exception in getBodySymptomsByName: $e"); + loggerService.logError("Exception in getSuggestions: $e"); loggerService.logError("StackTrace: $stackTrace"); - log("Exception: $e"); return Left(UnknownFailure(e.toString())); } } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index fe66cf7..73768fe 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -1,20 +1,26 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping_data.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class SymptomsCheckerViewModel extends ChangeNotifier { final SymptomsCheckerRepo symptomsCheckerRepo; final ErrorHandlerService errorHandlerService; + final AppState appState; SymptomsCheckerViewModel({ required this.symptomsCheckerRepo, required this.errorHandlerService, + required this.appState, }); // State variables @@ -29,16 +35,39 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // API loading states bool isBodySymptomsLoading = false; + bool isRiskFactorsLoading = false; + bool isSuggestionsLoading = false; + bool isTriageDiagnosisLoading = false; // API data storage - using API models directly + SymptomsUserDetailsResponseModel? symptomsUserDetailsResponseModel; BodySymptomResponseModel? bodySymptomResponse; + RiskAndSuggestionsResponseModel? riskFactorsResponse; + RiskAndSuggestionsResponseModel? suggestionsResponse; + TriageDataDetails? triageDataDetails; + + // Triage state + int? + _selectedTriageChoiceIndex; // Deprecated - keeping for backward compatibility + final Map _selectedTriageChoicesByItemId = + {}; // Map of itemId -> choiceIndex for multi-item questions + final List> _triageEvidenceList = + []; // Store triage evidence with proper format + int _triageQuestionCount = 0; // Track number of triage questions answered + + // Selected risk factors tracking + final Set _selectedRiskFactorIds = {}; + + // Selected Suggestions tracking + final Set _selectedSuggestionsIds = {}; // Selected symptoms tracking (organId -> Set of symptom IDs) final Map> _selectedSymptomsByOrgan = {}; // User Info Flow State int _userInfoCurrentPage = 0; - bool _isSinglePageEditMode = false; // Track if editing single page or full flow + bool _isSinglePageEditMode = + false; // Track if editing single page or full flow String? _selectedGender; DateTime? _dateOfBirth; int? _selectedAge; @@ -78,8 +107,54 @@ class SymptomsCheckerViewModel extends ChangeNotifier { String? get tooltipOrganId => _tooltipOrganId; + String get currentSessionAuthToken => + symptomsUserDetailsResponseModel?.tokenDetails?.authToken ?? ""; + + String get currentSessionId => + symptomsUserDetailsResponseModel?.sessionId ?? ""; + + // Triage-related getters + bool get shouldStopTriage => triageDataDetails?.shouldStop ?? false; + + bool get hasEmergencyEvidence => + triageDataDetails?.hasEmergencyEvidence ?? false; + + String? get currentInterviewToken => triageDataDetails?.interviewToken; + + TriageQuestion? get currentTriageQuestion => triageDataDetails?.question; + + List? get currentConditions => triageDataDetails?.conditions; + + int? get selectedTriageChoiceIndex => _selectedTriageChoiceIndex; + + /// Get the number of triage questions answered + int get triageQuestionCount => _triageQuestionCount; + + /// Get choice index for a specific item + int? getTriageChoiceForItem(String itemId) { + return _selectedTriageChoicesByItemId[itemId]; + } + + /// Check if all items in current question have been answered + bool get areAllTriageItemsAnswered { + if (currentTriageQuestion?.items == null || + currentTriageQuestion!.items!.isEmpty) { + return false; + } + + // Check if we have an answer for each item + for (var item in currentTriageQuestion!.items!) { + if (item.id != null && + !_selectedTriageChoicesByItemId.containsKey(item.id)) { + return false; + } + } + return true; + } + /// Get organs for current view - List get currentOrgans => OrganData.getOrgansForView(_currentView); + List get currentOrgans => + OrganData.getOrgansForView(_currentView); /// Get all selected organs from both views List get selectedOrgans { @@ -87,7 +162,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier { ...OrganData.frontViewOrgans, ...OrganData.backViewOrgans, ]; - return allOrgans.where((organ) => _selectedOrganIds.contains(organ.id)).toList(); + return allOrgans + .where((organ) => _selectedOrganIds.contains(organ.id)) + .toList(); } /// Check if any organs are selected @@ -104,15 +181,40 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } int get totalSelectedSymptomsCount { - return _selectedSymptomsByOrgan.values.fold(0, (sum, symptomIds) => sum + symptomIds.length); + return _selectedSymptomsByOrgan.values + .fold(0, (sum, symptomIds) => sum + symptomIds.length); } bool get hasSelectedSymptoms { - return _selectedSymptomsByOrgan.values.any((symptomIds) => symptomIds.isNotEmpty); + return _selectedSymptomsByOrgan.values + .any((symptomIds) => symptomIds.isNotEmpty); + } + + /// Get risk factors list + List get riskFactorsList { + return riskFactorsResponse?.dataDetails ?? []; + } + + /// Check if any risk factors are selected + bool get hasSelectedRiskFactors => _selectedRiskFactorIds.isNotEmpty; + + /// Get selected risk factors count + int get selectedRiskFactorsCount => _selectedRiskFactorIds.length; + + /// Check if any risk factors are selected + bool get hasSelectedSuggestions => _selectedSuggestionsIds.isNotEmpty; + + /// Get selected risk factors count + int get selectedSuggestionsCount => _selectedSuggestionsIds.length; + + /// Get risk factors list + List get suggestionsList { + return suggestionsResponse?.dataDetails ?? []; } void toggleView() { - _currentView = _currentView == BodyView.front ? BodyView.back : BodyView.front; + _currentView = + _currentView == BodyView.front ? BodyView.back : BodyView.front; notifyListeners(); } @@ -122,6 +224,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } void toggleOrganSelection(String organId) { + if (selectedOrganIds.isEmpty && _isBottomSheetExpanded == false) { + toggleBottomSheet(); + } + if (_selectedOrganIds.contains(organId)) { _selectedOrganIds.remove(organId); } else { @@ -131,6 +237,11 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Show tooltip _showTooltip(organId); + if (_selectedOrganIds.isEmpty) { + _isBottomSheetExpanded = false; + notifyListeners(); + } + notifyListeners(); } @@ -157,6 +268,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { void removeOrgan(String organId) { _selectedOrganIds.remove(organId); notifyListeners(); + if (_selectedOrganIds.isEmpty) { + _isBottomSheetExpanded = false; + notifyListeners(); + } } void clearAllSelections() { @@ -164,7 +279,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } - void toggleBottomSheet() { + void toggleBottomSheet({bool? value}) { + if (value != null) { + _isBottomSheetExpanded = value; + notifyListeners(); + return; + } _isBottomSheetExpanded = !_isBottomSheetExpanded; notifyListeners(); } @@ -197,7 +317,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { return; } - List organNames = selectedOrgans.map((organ) => organ.name).toList(); + List organNames = + selectedOrgans.map((organ) => organ.name).toList(); await getBodySymptomsByName( organNames: organNames, @@ -247,7 +368,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } } - if (matchingOrganId != null && _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { + if (matchingOrganId != null && + _selectedSymptomsByOrgan.containsKey(matchingOrganId)) { final selectedIds = _selectedSymptomsByOrgan[matchingOrganId]!; if (organResult.bodySymptoms != null) { @@ -268,11 +390,492 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } + // Risk Factors Methods + + /// Toggle risk factor selection + void toggleRiskFactorSelection(String riskFactorId) { + if (riskFactorId == "not_applicable") { + // "Not applicable" is mutually exclusive: if selected, clear all others + if (_selectedRiskFactorIds.contains(riskFactorId)) { + _selectedRiskFactorIds.remove(riskFactorId); + } else { + _selectedRiskFactorIds + ..clear() + ..add(riskFactorId); + } + } else { + _selectedRiskFactorIds.remove("not_applicable"); + if (!_selectedRiskFactorIds.add(riskFactorId)) { + _selectedRiskFactorIds.remove(riskFactorId); + } + } + + notifyListeners(); + } + + /// Check if risk factor is selected + bool isRiskFactorSelected(String riskFactorId) { + return _selectedRiskFactorIds.contains(riskFactorId); + } + + /// Get all selected risk factors + List getAllSelectedRiskFactors() { + return riskFactorsList + .where((factor) => + factor.id != null && _selectedRiskFactorIds.contains(factor.id)) + .toList(); + } + + /// Clear all risk factor selections + void clearAllRiskFactorSelections() { + _selectedRiskFactorIds.clear(); + notifyListeners(); + } + + /// Fetch risk factors based on selected symptoms + Future fetchRiskFactors({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Get all selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + + if (selectedSymptoms.isEmpty) { + if (onError != null) { + onError('No symptoms selected'); + } + return; + } + + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Extract symptom IDs + List evidenceIds = + selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + + await getRiskFactors( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + sessionId: currentSessionId, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Call Risk Factors API + Future getRiskFactors({ + required int age, + required String sex, + required String sessionId, + required List evidenceIds, + required String language, + Function(RiskAndSuggestionsResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isRiskFactorsLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getRiskFactors( + age: age, + sex: sex, + evidenceIds: evidenceIds, + language: language, + sessionId: sessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, + ); + + result.fold( + (failure) async { + isRiskFactorsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isRiskFactorsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + riskFactorsResponse = apiResponse.data; + + if (riskFactorsResponse != null && + riskFactorsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = + RiskAndSuggestionsItemModel( + id: "not_applicable", + commonName: "Not Applicable", + name: "Not Applicable", + language: appState.isArabic() ? 'ar' : 'en', + type: null, + ); + riskFactorsResponse!.dataDetails!.add(riskFactorItem); + } + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch risk factors'); + } + } + }, + ); + } + + // Suggestions Methods + + /// Toggle suggestions selection + void toggleSuggestionsSelection(String suggestionsId) { + if (suggestionsId == "not_applicable") { + // "Not applicable" is mutually exclusive: if selected, clear all others + if (_selectedSuggestionsIds.contains(suggestionsId)) { + _selectedSuggestionsIds.remove(suggestionsId); + } else { + _selectedSuggestionsIds + ..clear() + ..add(suggestionsId); + } + } else { + _selectedSuggestionsIds.remove("not_applicable"); + if (!_selectedSuggestionsIds.add(suggestionsId)) { + _selectedSuggestionsIds.remove(suggestionsId); + } + } + + notifyListeners(); + } + + /// Check if risk factor is selected + bool isSuggestionsSelected(String riskFactorId) { + return _selectedSuggestionsIds.contains(riskFactorId); + } + + /// Get all selected risk factors + List getAllSelectedSuggestions() { + return suggestionsList + .where((factor) => + factor.id != null && _selectedSuggestionsIds.contains(factor.id)) + .toList(); + } + + /// Clear all risk factor selections + void clearAllSuggestionsSelections() { + _selectedSuggestionsIds.clear(); + notifyListeners(); + } + + /// Get all evidence IDs (symptoms + risk factors + suggestions) for triage/diagnosis + List getAllEvidenceIds() { + List evidenceIds = []; + + // Add selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + evidenceIds + .addAll(selectedSymptoms.where((s) => s.id != null).map((s) => s.id!)); + + // Add selected risk factors (excluding "not_applicable") + final selectedRiskFactors = getAllSelectedRiskFactors(); + evidenceIds.addAll(selectedRiskFactors + .where((rf) => rf.id != null && rf.id != "not_applicable") + .map((rf) => rf.id!)); + + // Add selected suggestions (excluding "not_applicable") + final selectedSuggestions = getAllSelectedSuggestions(); + evidenceIds.addAll(selectedSuggestions + .where((s) => s.id != null && s.id != "not_applicable") + .map((s) => s.id!)); + + return evidenceIds; + } + + /// Fetch risk factors based on selected symptoms + Future fetchSuggestions({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Get all selected symptoms + final selectedSymptoms = getAllSelectedSymptoms(); + + if (selectedSymptoms.isEmpty) { + if (onError != null) { + onError('No symptoms selected'); + } + return; + } + + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Extract symptom IDs + List evidenceIds = + selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList(); + + // Get all selected symptoms + final selectedRisks = getAllSelectedRiskFactors(); + + if (selectedRisks.isNotEmpty) { + List evidenceRisksIds = selectedRisks + .where((s) => s.id != null && s.id != "not_applicable") + .map((s) => s.id!) + .toList(); + evidenceIds.addAll(evidenceRisksIds); + } + + await getSuggestions( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Call Suggestions API + Future getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + Function(RiskAndSuggestionsResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isSuggestionsLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getSuggestions( + age: age, + sex: sex, + evidenceIds: evidenceIds, + language: language, + sessionId: currentSessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, + ); + + result.fold( + (failure) async { + isSuggestionsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isSuggestionsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + suggestionsResponse = apiResponse.data; + + if (suggestionsResponse != null && + suggestionsResponse!.dataDetails != null) { + RiskAndSuggestionsItemModel riskFactorItem = + RiskAndSuggestionsItemModel( + id: "not_applicable", + commonName: "Not Applicable", + name: "Not Applicable", + language: appState.isArabic() ? 'ar' : 'en', + type: null, + ); + suggestionsResponse!.dataDetails!.add(riskFactorItem); + } + + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch risk factors'); + } + } + }, + ); + } + + /// Call Diagnosis API for Triage - This is called iteratively until shouldStop is true + Future getDiagnosisForTriage({ + required int age, + required String sex, + required List evidenceIds, + List>? triageEvidence, + required String language, + Function(TriageDataDetails)? onSuccess, + Function(String)? onError, + }) async { + isTriageDiagnosisLoading = true; + notifyListeners(); + + final result = await symptomsCheckerRepo.getDiagnosisForTriage( + age: age, + sex: sex, + evidenceIds: evidenceIds, + triageEvidence: triageEvidence, + language: language, + sessionId: currentSessionId, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, + ); + + result.fold( + (failure) async { + isTriageDiagnosisLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isTriageDiagnosisLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + triageDataDetails = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data!); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch diagnosis'); + } + } + }, + ); + } + + /// Convenience method to start or continue the triage process + /// This automatically uses all selected evidence (symptoms + risk factors + suggestions) + Future startOrContinueTriage({ + Function()? onSuccess, + Function(String)? onError, + }) async { + // Validate user info + if (_selectedAge == null || _selectedGender == null) { + if (onError != null) { + onError('User information is incomplete'); + } + return; + } + + // Get all evidence IDs + final evidenceIds = getAllEvidenceIds(); + + if (evidenceIds.isEmpty) { + if (onError != null) { + onError('No evidence selected'); + } + return; + } + + await getDiagnosisForTriage( + age: _selectedAge!, + sex: _selectedGender!.toLowerCase(), + evidenceIds: evidenceIds, + language: appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + if (onSuccess != null) { + onSuccess(); + } + }, + onError: (error) { + if (onError != null) { + onError(error); + } + }, + ); + } + + /// Select a triage choice option (for backward compatibility with single-item questions) + void selectTriageChoice(int choiceIndex) { + _selectedTriageChoiceIndex = choiceIndex; + notifyListeners(); + } + + /// Select a choice for a specific item (for multi-item questions) + void selectTriageChoiceForItem(String itemId, int choiceIndex) { + _selectedTriageChoicesByItemId[itemId] = choiceIndex; + notifyListeners(); + } + + /// Reset triage choice selection and increment question count + void resetTriageChoice() { + _selectedTriageChoiceIndex = null; + _selectedTriageChoicesByItemId.clear(); + _triageQuestionCount++; // Increment question count + notifyListeners(); + } + + /// Add triage evidence in the proper format + void addTriageEvidence(String itemId, String choiceId) { + _triageEvidenceList.add({ + "id": itemId, + "choice_id": choiceId, + "source": "triage", + }); + notifyListeners(); + } + + /// Get all triage evidence + List> getTriageEvidence() { + return List.from(_triageEvidenceList); + } + + /// Clear triage evidence + void clearTriageEvidence() { + _triageEvidenceList.clear(); + notifyListeners(); + } + void reset() { _currentView = BodyView.front; _selectedOrganIds.clear(); _selectedSymptomsByOrgan.clear(); + _selectedRiskFactorIds.clear(); + _selectedSuggestionsIds.clear(); + _triageEvidenceList.clear(); + _selectedTriageChoicesByItemId.clear(); + _triageQuestionCount = 0; // Reset question count bodySymptomResponse = null; + riskFactorsResponse = null; + suggestionsResponse = null; + triageDataDetails = null; + isTriageDiagnosisLoading = false; + _selectedTriageChoiceIndex = null; _isBottomSheetExpanded = false; _tooltipTimer?.cancel(); _tooltipOrganId = null; @@ -338,7 +941,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Calculate age from date of birth final now = DateTime.now(); int age = now.year - dateOfBirth.year; - if (now.month < dateOfBirth.month || (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { + if (now.month < dateOfBirth.month || + (now.month == dateOfBirth.month && now.day < dateOfBirth.day)) { age--; } _selectedAge = age; @@ -377,6 +981,44 @@ class SymptomsCheckerViewModel extends ChangeNotifier { }; } + Future getSymptomsUserDetails({ + required String userName, + required String password, + Function()? onSuccess, + Function(String)? onError, + }) async { + isBodySymptomsLoading = true; + notifyListeners(); + final result = await symptomsCheckerRepo.getUserDetails( + userName: userName, password: password); + + result.fold( + (failure) async { + isBodySymptomsLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isBodySymptomsLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + symptomsUserDetailsResponseModel = apiResponse.data; + notifyListeners(); + if (onSuccess != null) { + onSuccess(); + } + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? 'Failed to fetch symptoms'); + } + } + }, + ); + } + Future getBodySymptomsByName({ required List organNames, Function(BodySymptomResponseModel)? onSuccess, @@ -387,6 +1029,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier { final result = await symptomsCheckerRepo.getBodySymptomsByName( organNames: organNames, + userSessionToken: currentSessionAuthToken, + gender: (selectedGender ?? "Male").toLowerCase() == "male" ? 1 : 2, ); result.fold( diff --git a/lib/features/water_monitor/models/insert_user_activity_request_model.dart b/lib/features/water_monitor/models/insert_user_activity_request_model.dart new file mode 100644 index 0000000..a0af649 --- /dev/null +++ b/lib/features/water_monitor/models/insert_user_activity_request_model.dart @@ -0,0 +1,26 @@ +class InsertUserActivityRequestModel { + String? identificationNo; + String? mobileNumber; + num? quantityIntake; + + InsertUserActivityRequestModel({ + this.identificationNo, + this.mobileNumber, + this.quantityIntake, + }); + + Map toJson() { + final map = {}; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (quantityIntake != null) map['QuantityIntake'] = quantityIntake; + return map; + } + + factory InsertUserActivityRequestModel.fromJson(Map json) => InsertUserActivityRequestModel( + identificationNo: json['IdentificationNo']?.toString(), + mobileNumber: json['MobileNumber']?.toString(), + quantityIntake: json['QuantityIntake'], + ); +} + diff --git a/lib/features/water_monitor/models/undo_user_activity_request_model.dart b/lib/features/water_monitor/models/undo_user_activity_request_model.dart new file mode 100644 index 0000000..01dbd92 --- /dev/null +++ b/lib/features/water_monitor/models/undo_user_activity_request_model.dart @@ -0,0 +1,26 @@ +class UndoUserActivityRequestModel { + num? progress; + String? mobileNumber; + String? identificationNo; + + UndoUserActivityRequestModel({ + this.progress, + this.mobileNumber, + this.identificationNo, + }); + + Map toJson() { + final map = {}; + if (progress != null) map['Progress'] = progress; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + return map; + } + + factory UndoUserActivityRequestModel.fromJson(Map json) => UndoUserActivityRequestModel( + progress: json['Progress'], + mobileNumber: json['MobileNumber']?.toString(), + identificationNo: json['IdentificationNo']?.toString(), + ); +} + diff --git a/lib/features/water_monitor/models/update_user_detail_request_model.dart b/lib/features/water_monitor/models/update_user_detail_request_model.dart new file mode 100644 index 0000000..33dbbb2 --- /dev/null +++ b/lib/features/water_monitor/models/update_user_detail_request_model.dart @@ -0,0 +1,88 @@ +class UpdateUserDetailRequestModel { + // User detail fields + String? activityID; + String? dOB; + String? email; + String? firstName; + String? lastName; + String? firstNameN; + String? middleName; + String? middleNameN; + String? lastNameN; + String? gender; + num? height; + bool? isHeightInCM; + bool? isWeightInKG; + String? zipCode; + num? weight; + bool? isNotificationOn; + String? mobileNumber; + String? identificationNo; + + UpdateUserDetailRequestModel({ + this.activityID, + this.dOB, + this.email, + this.firstName, + this.lastName, + this.firstNameN, + this.middleName, + this.middleNameN, + this.lastNameN, + this.gender, + this.height, + this.isHeightInCM, + this.isWeightInKG, + this.zipCode, + this.weight, + this.isNotificationOn, + this.mobileNumber, + this.identificationNo, + }); + + Map toJson() { + final map = {}; + + // User detail fields + if (activityID != null) map['ActivityID'] = activityID; + if (dOB != null) map['DOB'] = dOB; + if (email != null) map['Email'] = email; + if (firstName != null) map['FirstName'] = firstName; + if (lastName != null) map['LastName'] = lastName; + if (firstNameN != null) map['FirstNameN'] = firstNameN; + if (middleName != null) map['MiddleName'] = middleName; + if (middleNameN != null) map['MiddleNameN'] = middleNameN; + if (lastNameN != null) map['LastNameN'] = lastNameN; + if (gender != null) map['Gender'] = gender; + if (height != null) map['Height'] = height; + if (isHeightInCM != null) map['IsHeightInCM'] = isHeightInCM; + if (isWeightInKG != null) map['IsWeightInKG'] = isWeightInKG; + if (zipCode != null) map['ZipCode'] = zipCode; + if (weight != null) map['Weight'] = weight; + if (isNotificationOn != null) map['IsNotificationOn'] = isNotificationOn; + if (mobileNumber != null) map['MobileNumber'] = mobileNumber; + if (identificationNo != null) map['IdentificationNo'] = identificationNo; + return map; + } + + factory UpdateUserDetailRequestModel.fromJson(Map json) => UpdateUserDetailRequestModel( + activityID: json['ActivityID']?.toString(), + dOB: json['DOB']?.toString(), + email: json['Email']?.toString(), + firstName: json['FirstName']?.toString(), + lastName: json['LastName']?.toString(), + firstNameN: json['FirstNameN']?.toString(), + middleName: json['MiddleName']?.toString(), + middleNameN: json['MiddleNameN']?.toString(), + lastNameN: json['LastNameN']?.toString(), + gender: json['Gender']?.toString(), + height: json['Height'], + isHeightInCM: json['IsHeightInCM'] as bool?, + isWeightInKG: json['IsWeightInKG'] as bool?, + zipCode: json['ZipCode']?.toString(), + weight: json['Weight'], + isNotificationOn: json['IsNotificationOn'] as bool?, + mobileNumber: json['MobileNumber']?.toString(), + identificationNo: json['IdentificationNo']?.toString(), + ); +} diff --git a/lib/features/water_monitor/models/user_progress_models.dart b/lib/features/water_monitor/models/user_progress_models.dart new file mode 100644 index 0000000..667653e --- /dev/null +++ b/lib/features/water_monitor/models/user_progress_models.dart @@ -0,0 +1,111 @@ +/// Model for today's water progress data +class UserProgressForTodayModel { + num? quantityConsumed; + num? percentageConsumed; + num? percentageLeft; + num? quantityLimit; + + UserProgressForTodayModel({ + this.quantityConsumed, + this.percentageConsumed, + this.percentageLeft, + this.quantityLimit, + }); + + factory UserProgressForTodayModel.fromJson(Map json) => UserProgressForTodayModel( + quantityConsumed: json['QuantityConsumed'], + percentageConsumed: json['PercentageConsumed'], + percentageLeft: json['PercentageLeft'], + quantityLimit: json['QuantityLimit'], + ); + + Map toJson() { + return { + 'QuantityConsumed': quantityConsumed, + 'PercentageConsumed': percentageConsumed, + 'PercentageLeft': percentageLeft, + 'QuantityLimit': quantityLimit, + }; + } +} + +/// Model for weekly water progress data +class UserProgressForWeekModel { + int? dayNumber; + String? dayDate; + String? dayName; + num? percentageConsumed; + + UserProgressForWeekModel({ + this.dayNumber, + this.dayDate, + this.dayName, + this.percentageConsumed, + }); + + factory UserProgressForWeekModel.fromJson(Map json) => UserProgressForWeekModel( + dayNumber: json['DayNumber'] as int?, + dayDate: json['DayDate']?.toString(), + dayName: json['DayName']?.toString(), + percentageConsumed: json['PercentageConsumed'], + ); + + Map toJson() { + return { + 'DayNumber': dayNumber, + 'DayDate': dayDate, + 'DayName': dayName, + 'PercentageConsumed': percentageConsumed, + }; + } +} + +/// Model for monthly water progress data +class UserProgressForMonthModel { + int? monthNumber; + String? monthName; + num? percentageConsumed; + + UserProgressForMonthModel({ + this.monthNumber, + this.monthName, + this.percentageConsumed, + }); + + factory UserProgressForMonthModel.fromJson(Map json) => UserProgressForMonthModel( + monthNumber: json['MonthNumber'] as int?, + monthName: json['MonthName']?.toString(), + percentageConsumed: json['PercentageConsumed'], + ); + + Map toJson() { + return { + 'MonthNumber': monthNumber, + 'MonthName': monthName, + 'PercentageConsumed': percentageConsumed, + }; + } +} + +/// Model for user progress history data +class UserProgressHistoryModel { + num? quantity; + String? createdDate; + + UserProgressHistoryModel({ + this.quantity, + this.createdDate, + }); + + factory UserProgressHistoryModel.fromJson(Map json) => UserProgressHistoryModel( + quantity: json['Quantity'], + createdDate: json['CreatedDate']?.toString(), + ); + + Map toJson() { + return { + 'Quantity': quantity, + 'CreatedDate': createdDate, + }; + } +} diff --git a/lib/features/water_monitor/models/water_cup_model.dart b/lib/features/water_monitor/models/water_cup_model.dart new file mode 100644 index 0000000..b86fae4 --- /dev/null +++ b/lib/features/water_monitor/models/water_cup_model.dart @@ -0,0 +1,47 @@ +class WaterCupModel { + final String id; + final String name; + final int capacityMl; + final String iconPath; // or use IconData if you prefer + final bool isDefault; + + WaterCupModel({ + required this.id, + required this.name, + required this.capacityMl, + required this.iconPath, + this.isDefault = false, + }); + + WaterCupModel copyWith({ + String? id, + String? name, + int? capacityMl, + String? iconPath, + bool? isDefault, + }) { + return WaterCupModel( + id: id ?? this.id, + name: name ?? this.name, + capacityMl: capacityMl ?? this.capacityMl, + iconPath: iconPath ?? this.iconPath, + isDefault: isDefault ?? this.isDefault, + ); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'capacityMl': capacityMl, + 'iconPath': iconPath, + 'isDefault': isDefault, + }; + + factory WaterCupModel.fromJson(Map json) => WaterCupModel( + id: json['id'], + name: json['name'], + capacityMl: json['capacityMl'], + iconPath: json['iconPath'], + isDefault: json['isDefault'] ?? false, + ); +} diff --git a/lib/features/water_monitor/water_monitor_repo.dart b/lib/features/water_monitor/water_monitor_repo.dart new file mode 100644 index 0000000..14a74a7 --- /dev/null +++ b/lib/features/water_monitor/water_monitor_repo.dart @@ -0,0 +1,333 @@ +import 'dart:developer'; + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Progress types to request different ranges from the progress API. +enum ProgressType { today, week, month } + +abstract class WaterMonitorRepo { + /// Fetch user details for water monitoring. + /// The request will include the standard payload injected by ApiClient and + /// additionally these three parameters: Progress, MobileNumber, IdentificationNo. + Future>> getUserDetailsForWaterMonitoring({ + required num progress, + required String mobileNumber, + required String identificationNo, + }); + + /// Fetch user progress for water monitoring (H2O_GetUserProgress). + Future>> getUserProgressForWaterMonitoring({ + required ProgressType progressType, + required String mobileNumber, + required String identificationNo, + }); + + /// Update user details for water monitoring. + Future>> updateOrInsertUserDetailForWaterMonitoring( + {required UpdateUserDetailRequestModel requestModel, bool isUpdate = false}); + + /// Insert user activity (water intake). + Future>> insertUserActivity({ + required InsertUserActivityRequestModel requestModel, + }); + + /// Undo last user activity. + Future>> undoUserActivity({ + required UndoUserActivityRequestModel requestModel, + }); +} + +class WaterMonitorRepoImp implements WaterMonitorRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + WaterMonitorRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>> getUserDetailsForWaterMonitoring({ + required num progress, + required String mobileNumber, + required String identificationNo, + }) async { + Map request = { + "Progress": progress, + "MobileNumber": mobileNumber, + "IdentificationNo": identificationNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.h2oGetUserDetail, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract only the specific key from the API response as requested + dynamic extracted; + if (response is Map && response.containsKey('UserDetailData_New')) { + extracted = response['UserDetailData_New']; + } else { + extracted = null; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getUserProgressForWaterMonitoring({ + required ProgressType progressType, + required String mobileNumber, + required String identificationNo, + }) async { + final progressValue = progressType == ProgressType.today ? 1 : (progressType == ProgressType.week ? 2 : 3); + + Map request = { + "Progress": progressValue, + "MobileNumber": mobileNumber, + "IdentificationNo": identificationNo, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.h2oGetUserProgress, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract progress data and history data + dynamic extracted; + dynamic historyData; + + if (response is Map) { + // Extract history data (available for all progress types) + if (response.containsKey('UserProgressHistoryData')) { + historyData = response['UserProgressHistoryData']; + } + + // Extract progress data based on type + switch (progressType) { + case ProgressType.today: + if (response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } + break; + case ProgressType.week: + if (response.containsKey('UserProgressForWeekData')) { + extracted = response['UserProgressForWeekData']; + } + break; + case ProgressType.month: + if (response.containsKey('UserProgressForMonthData')) { + extracted = response['UserProgressForMonthData']; + } + break; + } + + // fallbacks + if (extracted == null) { + if (response.containsKey('UserProgress')) { + extracted = response['UserProgress']; + } else if (response.containsKey('UserProgressData_New')) { + extracted = response['UserProgressData_New']; + } else { + extracted = response; + } + } + } else { + extracted = response; + } + + // Package both progress data and history data + final combinedData = { + 'progressData': extracted, + 'historyData': historyData, + }; + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: combinedData, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> updateOrInsertUserDetailForWaterMonitoring({ + required UpdateUserDetailRequestModel requestModel, + bool isUpdate = false, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + // Use different endpoint based on isUpdate flag + final endpoint = isUpdate ? ApiConsts.h2oUpdateUserDetail : ApiConsts.h2oInsertUserDetailsNew; + await apiClient.post( + endpoint, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + dynamic extracted; + if (response is Map && response.containsKey('UserDetailData_New')) { + extracted = response['UserDetailData_New']; + } else { + extracted = null; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> insertUserActivity({ + required InsertUserActivityRequestModel requestModel, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.h2oInsertUserActivity, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // Extract UserProgressForTodayData from the response + dynamic extracted; + if (response is Map && response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> undoUserActivity({ + required UndoUserActivityRequestModel requestModel, + }) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.h2oUndoUserActivity, + body: requestModel.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("response h2oUndoUserActivity: ${response.toString()}"); + // Extract UserProgressForTodayData from the response + dynamic extracted; + if (response is Map && response.containsKey('UserProgressForTodayData')) { + extracted = response['UserProgressForTodayData']; + } else { + extracted = response; + } + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: extracted, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/water_monitor/water_monitor_view_model.dart b/lib/features/water_monitor/water_monitor_view_model.dart new file mode 100644 index 0000000..d82712a --- /dev/null +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -0,0 +1,1350 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/insert_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/undo_user_activity_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/update_user_detail_request_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/user_progress_models.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/services/notification_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class WaterMonitorViewModel extends ChangeNotifier { + WaterMonitorRepo waterMonitorRepo; + ErrorHandlerService errorHandlerService; + + WaterMonitorViewModel({ + required this.waterMonitorRepo, + required this.errorHandlerService, + }); + + // Controllers + final TextEditingController nameController = TextEditingController(); + final TextEditingController heightController = TextEditingController(); + final TextEditingController weightController = TextEditingController(); + final TextEditingController ageController = TextEditingController(); + + // Units + final List heightUnits = ['cm', 'm', 'ft', 'in']; + final List weightUnits = ['kg', 'lb']; + + // Selected values + String _selectedHeightUnit = 'cm'; + String _selectedWeightUnit = 'kg'; + String _selectedActivityLevel = 'Lightly active'; + String _selectedNumberOfReminders = '3 Time'; + String _selectedGender = "Male"; + + // ConsumptionScreen + + String _selectedDuration = "Daily"; + bool _isGraphView = true; + + // Validation error message + String? _validationError; + + // Getters + String? get validationError => _validationError; + + String get selectedHeightUnit => _selectedHeightUnit; + + String get selectedWeightUnit => _selectedWeightUnit; + + String get selectedActivityLevel => _selectedActivityLevel; + + String get selectedNumberOfReminders => _selectedNumberOfReminders; + + String get selectedGender => _selectedGender; + + String get selectedDurationFilter => _selectedDuration; + + bool get isGraphView => _isGraphView; + + // Activity level options + List get activityLevels => + ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; + + // Reminder options + List get reminderOptions => ["1 Time", "2 Time", "3 Time", "4 Time", "5 Time", "6 Time"]; + + // Gender options + List get genderOptions => ["Male", "Female"]; + +//Duration Options + List get durationFilters => ["Daily", "Weekly", "Monthly"]; + + // Network/data + final AppState _appState = GetIt.instance(); + final NavigationService _navigationService = GetIt.instance(); + final CacheService _cacheService = GetIt.instance(); + + bool _isLoading = false; + dynamic _userDetailData; + bool _isWaterReminderEnabled = false; + + // Progress data lists + List _todayProgressList = []; + List _weekProgressList = []; + List _monthProgressList = []; + List _historyList = []; + + bool get isLoading => _isLoading; + + dynamic get userDetailData => _userDetailData; + + bool get isWaterReminderEnabled => _isWaterReminderEnabled; + + // Getters for progress data + List get todayProgressList => _todayProgressList; + + List get weekProgressList => _weekProgressList; + + List get monthProgressList => _monthProgressList; + + List get historyList => _historyList; + + // Get current progress list based on selected duration + dynamic get currentProgressData { + switch (_selectedDuration) { + case 'Daily': + return _todayProgressList; + case 'Weekly': + return _weekProgressList; + case 'Monthly': + return _monthProgressList; + default: + return _todayProgressList; + } + } + + // Initialize method to be called when needed + Future initialize() async { + _initializeDefaultCups(); + _loadReminderEnabledState(); + await fetchUserDetailsForMonitoring(); + // Fetch daily progress to get consumed amount and daily goal + await fetchUserProgressForMonitoring(); + } + + /// Load reminder enabled state from cache + void _loadReminderEnabledState() { + _isWaterReminderEnabled = _cacheService.getBool(key: CacheConst.waterReminderEnabled) ?? false; + log('Water reminder enabled state loaded: $_isWaterReminderEnabled'); + } + + /// Map selected duration to ProgressType enum + ProgressType _getProgressTypeFromDuration() { + switch (_selectedDuration) { + case 'Daily': + return ProgressType.today; + case 'Weekly': + return ProgressType.week; + case 'Monthly': + return ProgressType.month; + default: + return ProgressType.today; + } + } + + /// Map selected duration to ProgressType enum + int _getProgressIdFromDuration() { + switch (_selectedDuration) { + case 'Daily': + return 1; + case 'Weekly': + return 2; + case 'Monthly': + return 3; + default: + return 1; + } + } + + /// Fetch user progress data based on selected duration + Future fetchUserProgressForMonitoring() async { + try { + _isLoading = true; + notifyListeners(); + + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst(RegExp(r'^0'), ''); + final identification = authenticated.patientIdentificationNo ?? ''; + final progressType = _getProgressTypeFromDuration(); + + final result = await waterMonitorRepo.getUserProgressForWaterMonitoring( + progressType: progressType, + mobileNumber: mobile, + identificationNo: identification, + ); + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + log("User Progress Data ($_selectedDuration): ${apiModel.data.toString()}"); + + // Parse the response based on progress type + try { + // Extract progressData and historyData from combined response + dynamic progressData; + dynamic historyData; + + if (apiModel.data is Map && apiModel.data.containsKey('progressData')) { + progressData = apiModel.data['progressData']; + historyData = apiModel.data['historyData']; + } else { + // Fallback to old structure + progressData = apiModel.data; + } + + // Parse history data (available for all progress types, especially for daily) + if (historyData != null && historyData is List) { + _historyList.clear(); + for (var item in historyData) { + if (item is Map) { + _historyList.add(UserProgressHistoryModel.fromJson(item as Map)); + } + } + log('History data parsed: ${_historyList.length} items'); + } + + if (progressData != null && progressData is List) { + switch (progressType) { + case ProgressType.today: + _todayProgressList.clear(); + for (var item in progressData) { + if (item is Map) { + _todayProgressList.add(UserProgressForTodayModel.fromJson(item as Map)); + } + } + + // Update consumed amount and daily goal from API response + if (_todayProgressList.isNotEmpty) { + final todayData = _todayProgressList.first; + if (todayData.quantityConsumed != null) { + _totalConsumedMl = todayData.quantityConsumed!.toInt(); + log('Updated consumed from API: $_totalConsumedMl ml'); + } + if (todayData.quantityLimit != null) { + _dailyGoalMl = todayData.quantityLimit!.toInt(); + log('Updated daily goal from API: $_dailyGoalMl ml'); + } + } + + break; + + case ProgressType.week: + _weekProgressList.clear(); + for (var item in progressData) { + if (item is Map) { + _weekProgressList.add(UserProgressForWeekModel.fromJson(item as Map)); + } + } + log('Week Progress: ${_weekProgressList.length} items'); + break; + + case ProgressType.month: + _monthProgressList.clear(); + for (var item in progressData) { + if (item is Map) { + _monthProgressList.add(UserProgressForMonthModel.fromJson(item as Map)); + } + } + log('Month Progress: ${_monthProgressList.length} items'); + break; + } + } + } catch (e) { + log('Error parsing progress data: $e'); + } + }); + } catch (e) { + log('Exception in fetchUserProgressForMonitoring: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future fetchUserDetailsForMonitoring({Function(dynamic)? onSuccess, Function(String)? onError}) async { + try { + _isLoading = true; + + notifyListeners(); + + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + if (onError != null) onError('User not authenticated'); + return; + } + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', ''); + final identification = authenticated.patientIdentificationNo ?? ''; + + final result = await waterMonitorRepo.getUserDetailsForWaterMonitoring( + progress: 1, + mobileNumber: mobile, + identificationNo: identification, + ); + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + _userDetailData = null; + if (onError != null) onError(failure.message); + }, + (apiModel) { + _userDetailData = apiModel.data; + + // Populate form fields from the fetched data + if (_userDetailData != null) { + _populateFormFields(_userDetailData); + } + + if (onSuccess != null) onSuccess(_userDetailData); + }); + } catch (e) { + _userDetailData = null; + if (onError != null) onError(e.toString()); + } finally { + _isLoading = false; + notifyListeners(); + + if (_userDetailData == null) { + try { + _navigationService.pushAndReplace(AppRoutes.waterMonitorSettingsPage); + } catch (navErr) { + log('Navigation to water monitor settings failed: $navErr'); + } + } + } + } + + /// Populates form fields from the API response data + void _populateFormFields(dynamic data) { + if (data == null) return; + + try { + // Parse the response and populate fields + if (data is Map) { + // Name + if (data['FirstName'] != null) { + nameController.text = data['FirstName'].toString(); + } + + // Gender + if (data['Gender'] != null) { + final gender = data['Gender'].toString(); + if (gender == 'M' || gender == 'Male') { + _selectedGender = 'Male'; + } else if (gender == 'F' || gender == 'Female') { + _selectedGender = 'Female'; + } + } + + // Age - calculate from DOB if available + if (data['DOB'] != null) { + final dob = data['DOB'].toString(); + final age = _calculateAgeFromDOB(dob); + if (age > 0) { + ageController.text = age.toString(); + } + } + + // Height + if (data['Height'] != null) { + heightController.text = data['Height'].toString(); + // Set height unit + if (data['IsHeightInCM'] != null) { + _selectedHeightUnit = data['IsHeightInCM'] == true ? 'cm' : 'in'; + } + } + + // Weight + if (data['Weight'] != null) { + weightController.text = data['Weight'].toString(); + // Set weight unit + if (data['IsWeightInKG'] != null) { + _selectedWeightUnit = data['IsWeightInKG'] == true ? 'kg' : 'lb'; + } + } + + // Activity Level - map ActivityID to activity level + if (data['ActivityID'] != null) { + final activityId = data['ActivityID'].toString(); + _selectedActivityLevel = _getActivityLevelFromID(activityId); + } + + // Number of reminders (if available in response) + // Note: This may not be in the response, keeping default if not present + + notifyListeners(); + } + } catch (e) { + log('Error populating form fields: $e'); + } + } + + /// Calculate age from DOB string in format /Date(milliseconds+0300)/ + int _calculateAgeFromDOB(String dobString) { + try { + // Parse the /Date(milliseconds+0300)/ format + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dobString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + final dob = DateTime.fromMillisecondsSinceEpoch(milliseconds); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || (now.month == dob.month && now.day < dob.day)) { + age--; + } + return age; + } + } + } catch (e) { + log('Error calculating age from DOB: $e'); + } + return 0; + } + + /// Map activity ID to activity level string + String _getActivityLevelFromID(String activityId) { + switch (activityId) { + case '1': + return "Almost Inactive (no exercise)"; + case '2': + return "Lightly active"; + case '3': + return "Lightly active (1-3) days per week"; + case '4': + return "Super active (very hard exercise)"; + default: + return "Lightly active"; + } + } + + /// Populate form fields from authenticated user data (for new users) + void populateFromAuthenticatedUser() { + try { + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) return; + + // Name - use firstName if available + if (authenticated.firstName != null && authenticated.firstName!.isNotEmpty) { + nameController.text = authenticated.firstName!; + } + + // Gender - map from patient gender + if (authenticated.gender != null) { + final gender = (authenticated.gender == 1 ? 'male' : 'female').toLowerCase(); + if (gender.contains('m') || gender == 'male') { + _selectedGender = 'Male'; + } else if (gender.contains('f') || gender == 'female') { + _selectedGender = 'Female'; + } + } + + // Age - calculate from DOB if available + if (authenticated.dateofBirth != null && authenticated.dateofBirth!.isNotEmpty) { + final age = _calculateAgeFromDOB(authenticated.dateofBirth!); + if (age > 0) { + ageController.text = age.toString(); + } + } + + // Set default units and activity level + _selectedHeightUnit = 'cm'; + _selectedWeightUnit = 'kg'; + _selectedActivityLevel = 'Lightly active'; + _selectedNumberOfReminders = '3 Time'; + + log('Form fields populated from authenticated user: ${authenticated.firstName}'); + notifyListeners(); + } catch (e) { + log('Error populating from authenticated user: $e'); + } + } + + // Reset all fields to default + void resetFields() { + nameController.clear(); + heightController.clear(); + weightController.clear(); + ageController.clear(); + _selectedHeightUnit = 'cm'; + _selectedWeightUnit = 'kg'; + _selectedActivityLevel = 'Lightly active'; + _selectedNumberOfReminders = '3 Time'; + _selectedGender = "Male"; + notifyListeners(); + } + + // Setters with notification + void setFilterDuration(String duration) async { + _selectedDuration = duration; + notifyListeners(); + // Fetch new progress data when duration filter changes + await fetchUserProgressForMonitoring(); + } + + // Setters with notification + void setGraphView(bool value) { + _isGraphView = value; + notifyListeners(); + } + + // Setters with notification + void setGender(String gender) { + _selectedGender = gender; + notifyListeners(); + } + + void setHeightUnit(String unit) { + _selectedHeightUnit = unit; + notifyListeners(); + } + + void setWeightUnit(String unit) { + _selectedWeightUnit = unit; + notifyListeners(); + } + + void setActivityLevel(String level) { + _selectedActivityLevel = level; + notifyListeners(); + } + + void setNumberOfReminders(String number) { + _selectedNumberOfReminders = number; + notifyListeners(); + } + + // Validation methods + bool get isValid { + _validationError = null; // Clear previous error + + if (nameController.text.trim().isEmpty) { + _validationError = 'Name is required'; + return false; + } + + if (ageController.text.trim().isEmpty) { + _validationError = 'Age is required'; + return false; + } + + if (!_isAgeValid()) { + _validationError = validateAge(); + return false; + } + + if (heightController.text.trim().isEmpty) { + _validationError = 'Height is required'; + return false; + } + + if (!_isHeightValid()) { + _validationError = validateHeight(); + return false; + } + + if (weightController.text.trim().isEmpty) { + _validationError = 'Weight is required'; + return false; + } + + if (!_isWeightValid()) { + _validationError = validateWeight(); + return false; + } + + return true; + } + + bool _isAgeValid() { + final age = int.tryParse(ageController.text.trim()); + return age != null && age >= 11 && age <= 120; + } + + bool _isHeightValid() { + final height = double.tryParse(heightController.text.trim()); + return height != null && height > 0; + } + + bool _isWeightValid() { + final weight = double.tryParse(weightController.text.trim()); + return weight != null && weight > 0; + } + + String? validateAge() { + if (ageController.text.trim().isEmpty) { + return 'Age is required'.needTranslation; + } + final age = int.tryParse(ageController.text.trim()); + if (age == null) { + return 'Invalid age'.needTranslation; + } + if (age < 11 || age > 120) { + return 'Age must be between 11 and 120'.needTranslation; + } + return null; + } + + String? validateHeight() { + if (heightController.text.trim().isEmpty) { + return 'Height is required'.needTranslation; + } + final height = double.tryParse(heightController.text.trim()); + if (height == null || height <= 0) { + return 'Invalid height'.needTranslation; + } + return null; + } + + String? validateWeight() { + if (weightController.text.trim().isEmpty) { + return 'Weight is required'.needTranslation; + } + final weight = double.tryParse(weightController.text.trim()); + if (weight == null || weight <= 0) { + return 'Invalid weight'.needTranslation; + } + return null; + } + + String _getApiCompatibleGender() { + if (_selectedGender == "Female") { + return "F"; + } + return "M"; + } + + // Save settings + Future saveSettings() async { + if (!isValid) { + notifyListeners(); // Notify so error can be read + return false; + } + + try { + _isLoading = true; + _validationError = null; + notifyListeners(); + + // Determine if this is an update or insert + final isUpdate = _userDetailData != null; + + // Get authenticated user for mobile number and identification number + final authenticated = _appState.getAuthenticatedUser(); + final mobile = (authenticated?.mobileNumber?.replaceAll('+', '') ?? '').replaceFirst("0", ""); + final identification = authenticated?.patientIdentificationNo ?? ''; + final zipCode = authenticated?.zipCode ?? '966'; + final email = authenticated?.emailAddress ?? ''; + + // Get activity ID based on selected activity level + String activityID = _getActivityID(); + + // Create request model with user detail fields only + final requestModel = UpdateUserDetailRequestModel( + firstName: nameController.text.trim(), + lastName: nameController.text.trim(), + firstNameN: nameController.text.trim(), + middleName: '', + middleNameN: '', + lastNameN: nameController.text.trim(), + gender: _getApiCompatibleGender(), + dOB: _calculateDOBFromAge(), + height: double.tryParse(heightController.text.trim()), + isHeightInCM: _selectedHeightUnit == 'cm', + weight: double.tryParse(weightController.text.trim()), + isWeightInKG: _selectedWeightUnit == 'kg', + activityID: activityID, + isNotificationOn: true, + mobileNumber: mobile, + identificationNo: identification, + email: email, + zipCode: zipCode, + ); + + // Call the API + final result = await waterMonitorRepo.updateOrInsertUserDetailForWaterMonitoring( + requestModel: requestModel, + isUpdate: isUpdate, + ); + + return result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + _validationError = failure.message; + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) async { + // Update local data with response + _userDetailData = apiModel.data; + // Fetch daily progress to get the updated goal and consumed data from API + await fetchUserProgressForMonitoring(); + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + _validationError = e.toString(); + _isLoading = false; + notifyListeners(); + return false; + } + } + + // Helper method to get activity ID based on activity level + String _getActivityID() { + switch (_selectedActivityLevel) { + case "Almost Inactive (no exercise)": + return "1"; + case "Lightly active": + return "2"; + case "Lightly active (1-3) days per week": + return "3"; + case "Super active (very hard exercise)": + return "4"; + default: + return "2"; + } + } + + // Helper method to calculate DOB from age + String _calculateDOBFromAge() { + final age = int.tryParse(ageController.text.trim()) ?? 0; + if (age > 0) { + final currentYear = DateTime.now().year; + final birthYear = currentYear - age; + // Create a DateTime for January 1st of the birth year + final birthDate = DateTime(birthYear, 1, 1); + // Convert to API format: /Date(milliseconds+0300)/ using DateUtil + return DateUtil.convertDateToString(birthDate); + } + return ""; + } + + @override + void dispose() { + nameController.dispose(); + heightController.dispose(); + weightController.dispose(); + ageController.dispose(); + super.dispose(); + } + + List _cups = []; + String? _selectedCupId; + int _totalConsumedMl = 0; // Loaded from API + int _dailyGoalMl = 0; // Loaded from API + + // Calibration: portion of the bottle SVG height that is fillable (0.0 - 1.0) + double _fillableHeightPercent = 0.7; + + List get cups => _cups; + + WaterCupModel? get selectedCup { + if (_cups.isEmpty) return null; + return _cups.firstWhere((c) => c.id == _selectedCupId, orElse: () => _cups.first); + } + + int get totalConsumedMl => _totalConsumedMl; + + int get dailyGoalMl => _dailyGoalMl; + + // Portion of bottle drawable height that is fillable. + double get fillableHeightPercent => _fillableHeightPercent; + + void setFillableHeightPercent(double v) { + _fillableHeightPercent = v.clamp(0.0, 1.0); + notifyListeners(); + } + + // Normalized progress in 0.0 - 1.0 (ensure double) + double get progress { + if (_dailyGoalMl == 0) return 0.0; + final p = _totalConsumedMl / _dailyGoalMl; + return p.clamp(0.0, 1.0).toDouble(); + } + + // Convenience percent (0 - 100) + double get progressPercent => (progress * 100.0).clamp(0.0, 100.0).toDouble(); + + // Calculate hydration status based on progress + String get hydrationStatus { + final percent = progressPercent; + + if (percent >= 90) { + return "Well Hydrated"; + } else if (percent >= 70) { + return "Hydrated"; + } else if (percent >= 50) { + return "Moderately Hydrated"; + } else if (percent >= 30) { + return "Slightly Dehydrated"; + } else { + return "Dehydrated"; + } + } + + // Get hydration status color + Color get hydrationStatusColor { + final percent = progressPercent; + + if (percent >= 90) { + return AppColors.successColor; // Dark Green + } else if (percent >= 70) { + return AppColors.successColor; // Green + } else if (percent >= 50) { + return AppColors.warningColorYellow; //orange + } else if (percent >= 30) { + return AppColors.warningColorYellow; // Orange + } else { + return AppColors.errorColor; // Red + } + } + + String get nextDrinkTime { + if (progressPercent >= 100) { + return "Goal Achieved!"; + } + + // Get number of reminders from selected string (e.g., "3 Time" -> 3) + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + + // Define waking hours (e.g., 6 AM to 10 PM = 16 hours) + const wakingHoursStart = 6; // 6 AM + const wakingHoursEnd = 22; // 10 PM + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + + // Calculate interval between drinks in hours + final intervalHours = totalWakingHours / remindersPerDay; + + // Get current time + final now = DateTime.now(); + final currentHour = now.hour + (now.minute / 60.0); + + // If before waking hours, next drink is at start time + if (currentHour < wakingHoursStart) { + return "${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + if (currentHour >= wakingHoursEnd) { + return "Tomorrow ${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // Calculate which interval we're in + final hoursSinceWakeup = currentHour - wakingHoursStart; + final currentInterval = (hoursSinceWakeup / intervalHours).floor(); + + // Calculate next drink time + final nextDrinkHour = wakingHoursStart + ((currentInterval + 1) * intervalHours); + + // If next drink time is past bedtime, show tomorrow + if (nextDrinkHour >= wakingHoursEnd) { + return "Tomorrow ${wakingHoursStart.toString().padLeft(2, '0')}:00 AM"; + } + + // Format the time + final hour = nextDrinkHour.floor(); + final minute = ((nextDrinkHour - hour) * 60).round(); + + // Convert to 12-hour format + final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + + return "${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period"; + } + + /// Get time until next drink in minutes + int get minutesUntilNextDrink { + if (progressPercent >= 100) return 0; + + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + const wakingHoursStart = 6; + const wakingHoursEnd = 22; + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + final intervalHours = totalWakingHours / remindersPerDay; + + final now = DateTime.now(); + final currentHour = now.hour + (now.minute / 60.0); + + if (currentHour < wakingHoursStart) { + final minutesUntil = ((wakingHoursStart - currentHour) * 60).round(); + return minutesUntil; + } + + if (currentHour >= wakingHoursEnd) { + final hoursUntilTomorrow = 24 - currentHour + wakingHoursStart; + return (hoursUntilTomorrow * 60).round(); + } + + final hoursSinceWakeup = currentHour - wakingHoursStart; + final currentInterval = (hoursSinceWakeup / intervalHours).floor(); + final nextDrinkHour = wakingHoursStart + ((currentInterval + 1) * intervalHours); + + if (nextDrinkHour >= wakingHoursEnd) { + final hoursUntilTomorrow = 24 - currentHour + wakingHoursStart; + return (hoursUntilTomorrow * 60).round(); + } + + final minutesUntil = ((nextDrinkHour - currentHour) * 60).round(); + return minutesUntil.clamp(0, 1440); + } + + // Allow updating consumed and goal through the vm so UI doesn't manipulate internal fields directly + void setTotalConsumedMl(int ml) { + _totalConsumedMl = ml; + notifyListeners(); + } + + void addConsumedMl(int ml) { + _totalConsumedMl = (_totalConsumedMl + ml).clamp(0, 1000000); + notifyListeners(); + } + + void subtractConsumedMl(int ml) { + _totalConsumedMl = (_totalConsumedMl - ml).clamp(0, 1000000); + notifyListeners(); + } + + void setDailyGoal(int ml) { + _dailyGoalMl = ml; + notifyListeners(); + } + + void _initializeDefaultCups() { + _cups = [ + WaterCupModel( + id: 'default_125', + name: '125ml', + capacityMl: 125, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_150', + name: '150ml', + capacityMl: 150, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_175', + name: '175ml', + capacityMl: 175, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_200', + name: '200ml', + capacityMl: 200, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_250', + name: '250ml', + capacityMl: 250, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + WaterCupModel( + id: 'default_300', + name: '300ml', + capacityMl: 300, + iconPath: AppAssets.cupEmpty, + isDefault: true, + ), + ]; + _selectedCupId = _cups.first.id; + notifyListeners(); + } + + void selectCup(String cupId) { + _selectedCupId = cupId; + notifyListeners(); + } + + void addCup(WaterCupModel cup) { + _cups.add(cup); + notifyListeners(); + } + + void updateCup(WaterCupModel updatedCup) { + final index = _cups.indexWhere((c) => c.id == updatedCup.id); + if (index != -1) { + _cups[index] = updatedCup; + notifyListeners(); + } + } + + // Public alias for deleting/removing a cup. Keeps API intention clear in UI code. + void removeCup(String cupId) { + deleteCup(cupId); + } + + void deleteCup(String cupId) { + final cup = _cups.firstWhere((c) => c.id == cupId); + if (cup.isDefault) return; // can't delete default cups + + _cups.removeWhere((c) => c.id == cupId); + if (_selectedCupId == cupId) { + _selectedCupId = _cups.first.id; + } + notifyListeners(); + } + + // Returns the currently selected cup capacity in ml (0 if none) + int get selectedCupCapacityMl => selectedCup?.capacityMl ?? 0; + + /// Increment the consumed amount by the currently selected cup capacity. + /// This centralizes business logic here so UI just calls this method. + void incrementBySelectedCup() { + if (selectedCup != null) { + addConsumedMl(selectedCup!.capacityMl); + } + } + + /// Decrement the consumed amount by the currently selected cup capacity. + /// Ensures value is clamped inside the VM (subtractConsumedMl already clamps). + void decrementBySelectedCup() { + if (selectedCup != null) { + subtractConsumedMl(selectedCup!.capacityMl); + } + } + + /// Insert user activity (record water intake) + Future insertUserActivity({required int quantityIntake}) async { + try { + _isLoading = true; + notifyListeners(); + + // Get authenticated user info + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return false; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst("0", ""); + final identification = authenticated.patientIdentificationNo ?? ''; + + // Create request model + final requestModel = InsertUserActivityRequestModel( + identificationNo: identification, + mobileNumber: mobile, + quantityIntake: quantityIntake, + ); + + // Call the API + final result = await waterMonitorRepo.insertUserActivity(requestModel: requestModel); + + return result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + log('Error inserting user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) async { + log("Insert user activity success: ${apiModel.data.toString()}"); + if (apiModel.data != null && apiModel.data is List && (apiModel.data as List).isNotEmpty) { + final progressData = (apiModel.data as List).first; + if (progressData is Map) { + // Update consumed amount + if (progressData.containsKey('QuantityConsumed')) { + final consumed = progressData['QuantityConsumed']; + if (consumed != null) { + _totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl; + log('Updated consumed after insert: $_totalConsumedMl ml'); + } + } + // Update daily goal + if (progressData.containsKey('QuantityLimit')) { + final limit = progressData['QuantityLimit']; + if (limit != null) { + _dailyGoalMl = (limit is num) ? limit.toInt() : int.tryParse(limit.toString()) ?? _dailyGoalMl; + log('Updated daily goal after insert: $_dailyGoalMl ml'); + } + } + } + + // Refresh progress data to ensure consistency + await fetchUserProgressForMonitoring(); + } + + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + log('Exception in insertUserActivity: $e'); + _isLoading = false; + notifyListeners(); + return false; + } + } + + /// Undo last user activity + Future undoUserActivity() async { + try { + _isLoading = true; + notifyListeners(); + + // Get authenticated user info + final authenticated = _appState.getAuthenticatedUser(); + if (authenticated == null) { + _isLoading = false; + notifyListeners(); + return false; + } + + final mobile = (authenticated.mobileNumber ?? '').replaceAll('+', '').replaceFirst("0", ""); + final identification = authenticated.patientIdentificationNo ?? ''; + + // Create request model + final requestModel = UndoUserActivityRequestModel( + progress: _getProgressIdFromDuration(), + mobileNumber: mobile, + identificationNo: identification, + ); + + // Call the API + final result = await waterMonitorRepo.undoUserActivity(requestModel: requestModel); + + return result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + log('Error undoing user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) async { + log("Undo user activity success: ${apiModel.data.toString()}"); + + // Update consumed amount and goal from the response + if (apiModel.data != null && apiModel.data is List && (apiModel.data as List).isNotEmpty) { + final progressData = (apiModel.data as List).first; + if (progressData is Map) { + // Update consumed amount + if (progressData.containsKey('QuantityConsumed')) { + final consumed = progressData['QuantityConsumed']; + if (consumed != null) { + _totalConsumedMl = (consumed is num) ? consumed.toInt() : int.tryParse(consumed.toString()) ?? _totalConsumedMl; + log('Updated consumed after undo: $_totalConsumedMl ml'); + } + } + // Update daily goal + if (progressData.containsKey('QuantityLimit')) { + final limit = progressData['QuantityLimit']; + if (limit != null) { + _dailyGoalMl = (limit is num) ? limit.toInt() : int.tryParse(limit.toString()) ?? _dailyGoalMl; + log('Updated daily goal after undo: $_dailyGoalMl ml'); + } + } + } + await fetchUserProgressForMonitoring(); + } + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + log('Exception in undoUserActivity: $e'); + _isLoading = false; + notifyListeners(); + return false; + } + } + + /// Schedule water reminders based on user's reminder settings + Future scheduleWaterReminders() async { + try { + final notificationService = getIt.get(); + + // Request permission first + final hasPermission = await notificationService.requestPermissions(); + if (!hasPermission) { + log('Notification permission denied'); + return false; + } + + // Calculate reminder times based on _selectedNumberOfReminders + final reminderTimes = _calculateReminderTimes(); + + if (reminderTimes.isEmpty) { + log('No reminder times calculated'); + return false; + } + + // Schedule water reminders + await notificationService.scheduleWaterReminders( + reminderTimes: reminderTimes, + title: 'Time to Drink Water! 💧'.needTranslation, + body: 'Stay hydrated! Drink ${selectedCupCapacityMl}ml of water.'.needTranslation, + ); + + // Save reminder enabled state to cache + _isWaterReminderEnabled = true; + await _cacheService.saveBool(key: CacheConst.waterReminderEnabled, value: true); + + log('Scheduled ${reminderTimes.length} water reminders successfully'); + notifyListeners(); + return true; + } catch (e) { + log('Exception in scheduleWaterReminders: $e'); + return false; + } + } + + /// Calculate reminder times based on selected number of reminders + List _calculateReminderTimes() { + try { + final remindersPerDay = int.tryParse(_selectedNumberOfReminders.replaceAll(' Time', '').trim()) ?? 3; + + const wakingHoursStart = 6; // 6 AM + const wakingHoursEnd = 22; // 10 PM + const totalWakingHours = wakingHoursEnd - wakingHoursStart; + + final intervalHours = totalWakingHours / remindersPerDay; + + List times = []; + final now = DateTime.now(); + + for (int i = 0; i < remindersPerDay; i++) { + final hourDecimal = wakingHoursStart + (i * intervalHours); + final hour = hourDecimal.floor(); + final minute = ((hourDecimal - hour) * 60).round(); + + final reminderTime = DateTime( + now.year, + now.month, + now.day, + hour, + minute, + ); + + times.add(reminderTime); + } + + return times; + } catch (e) { + log('Error calculating reminder times: $e'); + return []; + } + } + + /// Cancel all water reminders + Future cancelWaterReminders() async { + try { + final notificationService = GetIt.instance(); + + // Get pending notifications and cancel water reminders (IDs 5000-5999) + final pendingNotifications = await notificationService.getPendingNotifications(); + for (final notification in pendingNotifications) { + if (notification.id >= 5000 && notification.id < 6000) { + await notificationService.cancelNotification(notification.id); + } + } + + // Save reminder disabled state to cache + _isWaterReminderEnabled = false; + await _cacheService.saveBool(key: CacheConst.waterReminderEnabled, value: false); + + log('Cancelled all water reminders'); + notifyListeners(); + return true; + } catch (e) { + log('Exception in cancelWaterReminders: $e'); + return false; + } + } + + /// Get list of scheduled water reminder times + Future> getScheduledReminderTimes() async { + try { + final notificationService = GetIt.instance(); + final pendingNotifications = await notificationService.getPendingNotifications(); + + List times = []; + for (final notification in pendingNotifications) { + if (notification.id >= 5000 && notification.id < 6000) { + // Note: PendingNotificationRequest doesn't contain scheduled time + // We can only return the calculated times based on current settings + times = _calculateReminderTimes(); + break; + } + } + + return times; + } catch (e) { + log('Exception in getScheduledReminderTimes: $e'); + return []; + } + } + + /// Schedule a test notification after 5 seconds + /// Useful for testing notification functionality + Future scheduleTestNotification() async { + try { + final notificationService = GetIt.instance(); + + // Request permission first + final hasPermission = await notificationService.requestPermissions(); + if (!hasPermission) { + log('Notification permission denied for test notification'); + return false; + } + + // Schedule notification 5 seconds from now + final scheduledTime = DateTime.now().add(const Duration(seconds: 5)); + + await notificationService.scheduleNotification( + id: 9999, + // Use a unique ID for test notifications + title: 'Time to Drink Water! 💧'.needTranslation, + body: 'Stay hydrated! Drink ${selectedCupCapacityMl}ml of water.'.needTranslation, + scheduledDate: scheduledTime, + payload: 'test_notification', + ); + + log('Test notification scheduled for 5 seconds from now'); + return true; + } catch (e) { + log('Exception in scheduleTestNotification: $e'); + return false; + } + } +} diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index d76422b..ea0bcb8 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const english = 'english'; static const arabic = 'arabic'; static const login = 'login'; @@ -189,7 +189,6 @@ abstract class LocaleKeys { static const firstName = 'firstName'; static const middleName = 'middleName'; static const lastName = 'lastName'; - static const female = 'female'; static const preferredLanguage = 'preferredLanguage'; static const locationsRegister = 'locationsRegister'; static const ksa = 'ksa'; @@ -801,7 +800,7 @@ abstract class LocaleKeys { static const fullName = 'fullName'; static const married = 'married'; static const uae = 'uae'; - static const malE = 'male'; + static const malE = 'malE'; static const loginBy = 'loginBy'; static const loginByOTP = 'loginByOTP'; static const guest = 'guest'; @@ -876,4 +875,6 @@ abstract class LocaleKeys { static const walkin = 'walkin'; static const laserClinic = 'laserClinic'; static const continueString = 'continueString'; + static const covid_info = 'covid_info'; + } diff --git a/lib/main.dart b/lib/main.dart index 30714c6..23bb3e2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,14 +22,21 @@ import 'package:hmg_patient_app_new/features/lab/history/lab_history_viewmodel.d import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; -import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -68,7 +75,7 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - AppDependencies.addDependencies(); + await AppDependencies.addDependencies(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); @@ -106,6 +113,9 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), ), @@ -153,6 +163,24 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), @@ -175,11 +203,7 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Dr. AlHabib', builder: (context, mchild) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(1.0), - ), - child: mchild!); + return MediaQuery(data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0)), child: mchild!); }, showSemanticsDebugger: false, debugShowCheckedModeBanner: false, @@ -199,3 +223,4 @@ class MyApp extends StatelessWidget { } } // flutter pub run easy_localization:generate -S assets/langs -f keys -o locale_keys.g.dart + diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index a2b7475..bd81584 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -1,13 +1,11 @@ import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; -import 'package:device_calendar/device_calendar.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; @@ -35,13 +33,13 @@ import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_deta import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:maps_launcher/maps_launcher.dart'; import 'package:provider/provider.dart'; -import '../../core/dependencies.dart'; import '../medical_file/widgets/medical_file_card.dart'; class AppointmentDetailsPage extends StatefulWidget { @@ -66,7 +64,6 @@ class _AppointmentDetailsPageState extends State { scheduleMicrotask(() async { CalenderUtilsNew calendarUtils = await CalenderUtilsNew.instance; var doesExist = await calendarUtils.checkIfEventExist("${widget.patientAppointmentHistoryResponseModel.appointmentNo}"); - print("the appointment reminder exist $doesExist"); myAppointmentsViewModel.setAppointmentReminder(doesExist, widget.patientAppointmentHistoryResponseModel); setState((){ @@ -78,7 +75,6 @@ class _AppointmentDetailsPageState extends State { @override Widget build(BuildContext context) { - AppState appState = getIt.get(); myAppointmentsViewModel = Provider.of(context, listen: false); prescriptionsViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); @@ -139,10 +135,7 @@ class _AppointmentDetailsPageState extends State { }, onCancelTap: () async { myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); - var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); - setState(() { - myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); - }); + LoaderBottomSheet.showLoader(loadingText: "Cancelling Appointment, Please Wait...".needTranslation); await myAppointmentsViewModel.cancelAppointment( patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, @@ -162,6 +155,10 @@ class _AppointmentDetailsPageState extends State { isFullScreen: false, ); }); + var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); + setState(() { + myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + }); }, onRescheduleTap: () async { openDoctorScheduleCalendar(); @@ -171,90 +168,105 @@ class _AppointmentDetailsPageState extends State { !AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? Column( children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - "Appointment Status".needTranslation.toText16(isBold: true), - ], - ), - SizedBox(height: 4.h), - (!AppointmentType.isConfirmed(widget.patientAppointmentHistoryResponseModel) - ? "Not Confirmed".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500) - : "Confirmed".needTranslation.toText12(color: AppColors.successColor, fontWeight: FontWeight.w500)), - SizedBox(height: 16.h), - //TODO Add countdown timer in case of LiveCare Appointment - widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false - ? Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "The doctor will call you once the appointment time approaches." - .needTranslation - .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), - ], + Consumer(builder: (context, bookAppointmentsVM, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Appointment Status".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(height: 4.h), + (!AppointmentType.isConfirmed(widget.patientAppointmentHistoryResponseModel) + ? "Not Confirmed".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500) + : "Confirmed".needTranslation.toText12(color: AppColors.successColor, fontWeight: FontWeight.w500)), + SizedBox(height: 16.h), + //TODO Add countdown timer in case of LiveCare Appointment + widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false + ? Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "The doctor will call you once the appointment time approaches." + .needTranslation + .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ], + ), ), - ), - ], - ) - : Stack( - children: [ - ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(24.r), - // Todo: what is this???? Api Key??? 😲 - child: Image.network( - "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}", - fit: BoxFit.contain, + ], + ) + : Stack( + children: [ + ClipRRect( + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular(24.r), + // Todo: what is this???? Api Key??? 😲 + child: Image.network( + "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=350x165&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}", + fit: BoxFit.contain, + ), ), - ), - Positioned( - bottom: 0, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.785, - child: CustomButton( - text: "Get Directions".needTranslation, - onPressed: () { - MapsLauncher.launchCoordinates( - double.parse(widget.patientAppointmentHistoryResponseModel.latitude!), - double.parse(widget.patientAppointmentHistoryResponseModel.longitude!), - widget.patientAppointmentHistoryResponseModel.projectName); - }, - backgroundColor: AppColors.textColor.withValues(alpha: 0.8), - borderColor: AppointmentType.getNextActionButtonColor( - widget.patientAppointmentHistoryResponseModel.nextAction) - .withValues(alpha: 0.01), - textColor: AppColors.whiteColor, - fontSize: 14.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 40.h, - icon: AppAssets.directions_icon, - iconColor: AppColors.whiteColor, - iconSize: 14.h, - ).paddingAll(12.h), + Positioned( + bottom: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.785, + child: CustomButton( + text: "Get Directions".needTranslation, + onPressed: () { + MapsLauncher.launchCoordinates(double.parse(widget.patientAppointmentHistoryResponseModel.latitude!), + double.parse(widget.patientAppointmentHistoryResponseModel.longitude!), widget.patientAppointmentHistoryResponseModel.projectName); + }, + backgroundColor: AppColors.textColor.withValues(alpha: 0.8), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppColors.whiteColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppAssets.directions_icon, + iconColor: AppColors.whiteColor, + iconSize: 14.h, + ).paddingAll(12.h), + ), ), - ), - ], - ), - ], + ], + ), + SizedBox(height: 8.h), + bookAppointmentsViewModel.appointmentNearestGateResponseModel != null ? Wrap( + direction: Axis.horizontal, + spacing: 8.w, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: bookAppointmentsVM.isAppointmentNearestGateLoading + ? "Floor: Ground Floor" + : "Floor: ${getIt.get().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}", + ).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + AppCustomChipWidget( + labelText: + "Nearest Gate: ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") + .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + ], + ) : SizedBox.shrink(), + ], + ), ), - ), - ), + ); + }), SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -386,7 +398,7 @@ class _AppointmentDetailsPageState extends State { label: "${LocaleKeys.radiology.tr(context: context)} ${LocaleKeys.radiologySubtitle.tr(context: context)}", textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.allergy_info_icon, + svgIcon: AppAssets.radiology_icon, isLargeText: true, iconSize: 36.w, ).onPress(() async { diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index ccd7018..475ee70 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -401,7 +401,6 @@ class _AppointmentPaymentPageState extends State { appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), onSuccess: (value) async { if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) { - //TODO: Implement LiveCare Check-In API Call await myAppointmentsViewModel.insertLiveCareVIDARequest( clientRequestID: tamaraOrderID, patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, diff --git a/lib/presentation/appointments/appointment_queue_page.dart b/lib/presentation/appointments/appointment_queue_page.dart index f48f1eb..124bf25 100644 --- a/lib/presentation/appointments/appointment_queue_page.dart +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -42,7 +42,11 @@ class AppointmentQueuePage extends StatelessWidget { color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false, - side: BorderSide(color: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), width: 2.w), + side: BorderSide( + color: myAppointmentsVM.isAppointmentQueueDetailsLoading + ? AppColors.whiteColor + : Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), + width: 2.w), ), child: Padding( padding: EdgeInsets.all(16.h), @@ -59,16 +63,25 @@ class AppointmentQueuePage extends StatelessWidget { ), Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), ], - ), + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 10.h), - "Hala ${appState!.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), + "Hala ${appState!.getAuthenticatedUser()!.firstName}!!!" + .needTranslation + .toText16(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 8.h), - "Thank you for your patience, here is your queue number.".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + "Thank you for your patience, here is your queue number." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 8.h), - "IMD W-A-5".needTranslation.toText32(isBold: true), + myAppointmentsVM.currentPatientQueueDetails.queueNo! + .toText32(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 8.h), CustomButton( - text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus), + text: Utils.getCardButtonText( + myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), onPressed: () {}, backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), @@ -80,66 +93,82 @@ class AppointmentQueuePage extends StatelessWidget { height: 40.h, iconColor: AppColors.whiteColor, iconSize: 18.h, - ), + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), ], ), ), ), SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Serving Now".needTranslation.toText16(isBold: true), - SizedBox(height: 18.h), - ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: 3, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "IMD W-A-2".needTranslation.toText17(isBold: true), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - "Room: S2".toText12(fontWeight: FontWeight.w500), - SizedBox(width: 8.w), - AppCustomChipWidget( - deleteIcon: AppAssets.call_for_vitals, - labelText: "Call for vital signs".needTranslation, - iconColor: AppColors.primaryRedColor, - textColor: AppColors.primaryRedColor, - iconSize: 14.w, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), - ), - ], - ), - ], - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h), + myAppointmentsVM.patientQueueDetailsList.isNotEmpty + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, ), - ], - ), - ), - ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Serving Now" + .needTranslation + .toText16(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + SizedBox(height: 18.h), + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: myAppointmentsVM.patientQueueDetailsList.length, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + myAppointmentsVM.patientQueueDetailsList[index].queueNo!.toText17(isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + "Room: ${myAppointmentsVM.patientQueueDetailsList[index].roomNo}" + .toText12(fontWeight: FontWeight.w500), + SizedBox(width: 8.w), + AppCustomChipWidget( + deleteIcon: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppAssets.call_for_vitals + : AppAssets.call_for_doctor, + labelText: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? "Call for vital signs".needTranslation + : "Call for Doctor".needTranslation, + iconColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppColors.primaryRedColor + : AppColors.successColor, + textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppColors.primaryRedColor + : AppColors.successColor, + iconSize: 14.w, + backgroundColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 + ? AppColors.primaryRedColor.withValues(alpha: 0.1) + : AppColors.successColor.withValues(alpha: 0.1), + labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), + ), + ], + ), + ], + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h), + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + ], + ), + ), + ) + : SizedBox.shrink(), SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, - borderRadius: 20.h, + borderRadius: 24.4, hasShadow: true, ), child: Padding( @@ -162,19 +191,29 @@ class AppointmentQueuePage extends StatelessWidget { // Are there any side effects I should know about? // When should I come back for a follow-up? - "• ${"What can I do to improve my overall health?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + "• ${"What can I do to improve my overall health?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 4.h), - "• ${"Are there any routine screenings I should get?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + "• ${"Are there any routine screenings I should get?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 4.h), - "• ${"What is this medication for?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + "• ${"What is this medication for?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 4.h), - "• ${"Are there any side effects I should know about?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + "• ${"Are there any side effects I should know about?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 4.h), - "• ${"When should I come back for a follow-up?"}".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + "• ${"When should I come back for a follow-up?"}" + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 16.h), ], - ), + ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), ), ), ], diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 934aca4..b4c3630 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -19,6 +19,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointme import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; @@ -35,6 +36,7 @@ class MyAppointmentsPage extends StatefulWidget { } class _MyAppointmentsPageState extends State { + int? expandedIndex; late MyAppointmentsViewModel myAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; @@ -68,6 +70,9 @@ class _MyAppointmentsPageState extends State { CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { + setState(() { + expandedIndex = null; + }); myAppointmentsViewModel.onTabChange(index); myAppointmentsViewModel.updateListWRTTab(index); context.read().flush(); @@ -91,17 +96,55 @@ class _MyAppointmentsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 16.h), + // Clinic & Hospital Sort + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + myAppointmentsVM.setIsAppointmentsSortByClinic(true); + }, + backgroundColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + myAppointmentsVM.setIsAppointmentsSortByClinic(false); + }, + backgroundColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h), Visibility(visible: myAppointmentsVM.availableFilters.isNotEmpty, child: getAppointmentFilters(myAppointmentsVM)), - ListView.separated( - padding: EdgeInsets.only(top: 16.h), + SizedBox(height: 8.h), + // Expandable list + ListView.builder( + padding: EdgeInsets.only(top: 8.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: myAppointmentsVM.isMyAppointmentsLoading - ? 5 + ? 4 : filteredAppointmentList.isNotEmpty - ? filteredAppointmentList.length + ? myAppointmentsVM.patientAppointmentsViewList.length : 1, itemBuilder: (context, index) { + final isExpanded = expandedIndex == index; return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), @@ -123,15 +166,83 @@ class _MyAppointmentsPageState extends State { child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: filteredAppointmentList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: false, - isFromHomePage: false, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: + "${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + myAppointmentsVM.patientAppointmentsViewList[index].filterName!.toText16(isBold: true) + ], + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 0.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.map((appointment) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppointmentCard( + patientAppointmentHistoryResponseModel: appointment, + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: false, + ), + SizedBox(height: 8.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h).paddingSymmetrical(16.w, 0.h), + SizedBox(height: 8.h), + ], + ); + }), + ], + ), + ) + : SizedBox.shrink(), + ), + ], + ), ), - ).paddingSymmetrical(24.h, 0.h), + ), ), ), ) @@ -160,8 +271,7 @@ class _MyAppointmentsPageState extends State { ).paddingSymmetrical(48.h, 0.h), ); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), + ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 24.h), ], ); diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 51e9703..2c5d1b0 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -1,9 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -21,169 +18,325 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; +import '../../widgets/buttons/custom_button.dart'; -class MyDoctorsPage extends StatelessWidget { +class MyDoctorsPage extends StatefulWidget { MyDoctorsPage({super.key}); + @override + State createState() => _MyDoctorsPageState(); +} + +class _MyDoctorsPageState extends State { late MyAppointmentsViewModel myAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; + bool isSortByClinic = true; // default like lab page + int? expandedIndex; + final Map _groupKeys = {}; + @override Widget build(BuildContext context) { - AppState appState = getIt.get(); myAppointmentsViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.myDoctor.tr(context: context), - child: SingleChildScrollView( - child: Consumer(builder: (context, myAppointmentsVM, child) { + + return CollapsingListView( + title: LocaleKeys.myDoctor.tr(context: context), + child: Consumer(builder: (context, myAppointmentsVM, child) { + // build grouped lists from the flat list + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var doc in myAppointmentsVM.patientMyDoctorsList) { + final clinicKey = (doc.clinicName ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(doc); + + final hospitalKey = (doc.projectName ?? doc.projectID?.toString() ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(doc); + } + + final patientMyDoctorsByClinic = clinicMap.values.toList(); + final patientMyDoctorsByHospital = hospitalMap.values.toList(); + final patientMyDoctorsViewList = isSortByClinic ? patientMyDoctorsByClinic : patientMyDoctorsByHospital; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), + // Add Clinic / Hospital toggle buttons styled like LabOrdersPage + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + setState(() { + isSortByClinic = true; + expandedIndex = null; + }); + }, + backgroundColor: isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + + + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + setState(() { + isSortByClinic = false; + expandedIndex = null; + }); + }, + backgroundColor: isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + + ), + ], + ), + ), + SizedBox(height: 16.h), ListView.separated( scrollDirection: Axis.vertical, - itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : myAppointmentsVM.patientMyDoctorsList.length, + itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : (patientMyDoctorsViewList.isNotEmpty ? patientMyDoctorsViewList.length : 0), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.only(left: 24.h, right: 24.h), itemBuilder: (context, index) { - return myAppointmentsVM.isPatientMyDoctorsLoading - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( + if (myAppointmentsVM.isPatientMyDoctorsLoading) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.network( - "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", - width: 63.h, - height: 63.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: true), - SizedBox(width: 16.h), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), - SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), - AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), - ], - ), - ], - ), - ), - ], - ), - ], - ), - ), - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: true), + SizedBox(width: 16.h), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, + "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 63.h, - height: 63.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: false), - SizedBox(width: 16.h), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (myAppointmentsVM.patientMyDoctorsList[index].doctorName).toString().toText16(isBold: true).toShimmer2(isShow: false), - SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].clinicName).toShimmer2(isShow: false, width: 16.h), - AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].projectName).toShimmer2(isShow: false, width: 16.h), - ], - ), - ], - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "".toText16(), - Transform.flip( - flipX: appState.isArabic(), - child: - Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), + AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h), ], ), ], ), ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, - projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, - doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, - )); - LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + ], ), + ], + ), + ), + ); + } + + final group = patientMyDoctorsViewList[index]; + final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); + final isExpanded = expandedIndex == index; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); + if (key.currentContext != null && expandedIndex == index) { + Future.delayed(const Duration(milliseconds: 450), () { + if (key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.0, + ); + } + }); + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + key: _groupKeys.putIfAbsent(index, () => GlobalKey()), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget(labelText: "${group.length} ${'doctors'.needTranslation}"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + Text( + displayName, + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...group.map((doctor) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"), + width: 24.w, + height: 24.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Expanded( + child: (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""), + ), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + flex: 2, + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: "View Profile".needTranslation.tr(context: context), + onPressed: () async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: doctor?.clinicID ?? 0, + projectID: doctor?.projectID ?? 0, + doctorID: doctor?.doctorID ?? 0, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), ), - ); + ], + ), + ), + ); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), @@ -191,8 +344,6 @@ class MyDoctorsPage extends StatelessWidget { ], ); }), - ), - ), ); } } diff --git a/lib/presentation/appointments/widgets/AppointmentFilter.dart b/lib/presentation/appointments/widgets/AppointmentFilter.dart index 98da22d..6546c65 100644 --- a/lib/presentation/appointments/widgets/AppointmentFilter.dart +++ b/lib/presentation/appointments/widgets/AppointmentFilter.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:smooth_corner/smooth_corner.dart'; diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index f5ec31b..39e4e03 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -38,7 +38,7 @@ class AppointmentCard extends StatelessWidget { final MedicalFileViewModel? medicalFileViewModel; final ContactUsViewModel? contactUsViewModel; final BookAppointmentsViewModel bookAppointmentsViewModel; - + final bool isForRate; const AppointmentCard({ super.key, required this.patientAppointmentHistoryResponseModel, @@ -51,6 +51,7 @@ class AppointmentCard extends StatelessWidget { this.isForFeedback = false, this.medicalFileViewModel, this.contactUsViewModel, + this.isForRate =false }); @override @@ -63,11 +64,11 @@ class AppointmentCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(context, appState), + isForRate ? SizedBox(): _buildHeader(context, appState), SizedBox(height: 16.h), _buildDoctorRow(context), SizedBox(height: 16.h), - _buildActionArea(context, appState), + isForRate ? SizedBox(): _buildActionArea(context, appState), ], ), ), @@ -128,7 +129,7 @@ class AppointmentCard extends StatelessWidget { children: [ Image.network( isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.w, + width: 63.h, height: 63.h, fit: BoxFit.cover, ).circle(100.r).toShimmer2(isShow: isLoading), @@ -178,19 +179,28 @@ class AppointmentCard extends StatelessWidget { ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 12)}...' : patientAppointmentHistoryResponseModel.clinicName!), ).toShimmer2(isShow: isLoading), - AppCustomChipWidget(labelText: isLoading ? 'Olaya' : patientAppointmentHistoryResponseModel.projectName!).toShimmer2(isShow: isLoading), AppCustomChipWidget( + labelText: isLoading + ? 'Olaya' + : patientAppointmentHistoryResponseModel.projectName!.length > 15 + ? '${patientAppointmentHistoryResponseModel.projectName!.substring(0, 12)}...' + : patientAppointmentHistoryResponseModel.projectName!) + .toShimmer2(isShow: isLoading), + AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.appointment_calendar_icon, labelText: isLoading ? 'Cardiology' : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}", ).toShimmer2(isShow: isLoading), - AppCustomChipWidget( - isIconPNG: true, - icon: getIt.get().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, - iconSize: 18.h, - labelText: isLoading ? 'Cardiology' : "Patient: ${getIt.get().getAuthenticatedUser()!.firstName!}", - ).toShimmer2(isShow: isLoading), + + // AppCustomChipWidget( + // labelPadding: EdgeInsetsDirectional.only(start: -2.w, end: 6.w), + // isIconPNG: true, + // icon: getIt.get().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, + // iconSize: 18.h, + // labelText: isLoading ? 'Cardiology' : "Patient: ${getIt.get().getAuthenticatedUser()!.firstName!}", + // ).toShimmer2(isShow: isLoading), // if (!isFromMedicalReport) // AppCustomChipWidget( // icon: AppAssets.appointment_time_icon, @@ -235,7 +245,8 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.checkmark_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, @@ -261,7 +272,7 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), iconSize: 15.h, @@ -333,7 +344,8 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + // height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.ask_doctor_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, @@ -350,7 +362,7 @@ class AppointmentCard extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.rebook_appointment_icon, iconColor: AppColors.blackColor, iconSize: 16.h, @@ -366,6 +378,7 @@ class AppointmentCard extends StatelessWidget { ), ); } else { + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: patientAppointmentHistoryResponseModel.projectID, clinicID: patientAppointmentHistoryResponseModel.clinicID); Navigator.of(context) .push( CustomPageRoute( diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index f430447..d118a5e 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -14,7 +14,6 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_queue_page.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:barcode_scan2/barcode_scan2.dart'; @@ -22,7 +21,6 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/nfc/nfc_reader_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; class AppointmentCheckinBottomSheet extends StatelessWidget { AppointmentCheckinBottomSheet({super.key, required this.patientAppointmentHistoryResponseModel, required this.myAppointmentsViewModel}); @@ -60,7 +58,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { double dist = Utils.distance(value.latitude, value.longitude, double.parse(projectDetailListModel.latitude!), double.parse(projectDetailListModel.longitude!)).ceilToDouble() * 1000; print(dist); if (dist <= projectDetailListModel.geofenceRadius!) { - sendCheckInRequest(projectDetailListModel.checkInQrCode!, context); + sendCheckInRequest(projectDetailListModel.checkInQrCode!, 3, context); } else { showCommonBottomSheetWithoutHeight(context, title: "Error".needTranslation, @@ -79,7 +77,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { Future.delayed(const Duration(milliseconds: 500), () { showNfcReader(context, onNcfScan: (String nfcId) { Future.delayed(const Duration(milliseconds: 100), () { - sendCheckInRequest(nfcId, context); + sendCheckInRequest(nfcId, 1, context); }); }, onCancel: () {}); }); @@ -92,7 +90,7 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { ).onPress(() async { String onlineCheckInQRCode = (await BarcodeScanner.scan().then((value) => value.rawContent)); if (onlineCheckInQRCode != "") { - sendCheckInRequest(onlineCheckInQRCode, context); + sendCheckInRequest(onlineCheckInQRCode, 2, context); } else {} }), ], @@ -140,15 +138,16 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { ); } - void sendCheckInRequest(String scannedCode, BuildContext context) async { + void sendCheckInRequest(String scannedCode, int checkInType, BuildContext context) async { LoaderBottomSheet.showLoader(loadingText: "Processing Check-In...".needTranslation); await myAppointmentsViewModel.sendCheckInNfcRequest( patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, scannedCode: scannedCode, - checkInType: 2, + checkInType: checkInType, onSuccess: (apiResponse) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () { + showCommonBottomSheetWithoutHeight(context, title: "Success".needTranslation, child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () async { + await myAppointmentsViewModel.getPatientAppointmentQueueDetails(); Navigator.of(context).pop(); Navigator.pushAndRemoveUntil( context, diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 96d538b..ccf6674 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -47,7 +47,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Image.network( patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.w, + width: 63.h, height: 63.h, fit: BoxFit.cover, ).circle(100.r), @@ -99,6 +99,7 @@ class AppointmentDoctorCard extends StatelessWidget { labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), ), AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, labelText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang( DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), @@ -106,6 +107,7 @@ class AppointmentDoctorCard extends StatelessWidget { )}", ), AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, @@ -165,14 +167,14 @@ class AppointmentDoctorCard extends StatelessWidget { backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, textColor: AppColors.blackColor, - fontSize: 14.f, + fontSize: 12.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, icon: AppAssets.rebook_appointment_icon, iconColor: AppColors.blackColor, - iconSize: 16.h, + iconSize: 14.h, ); } else { return patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false diff --git a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart index 69cdca3..01aab4e 100644 --- a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart +++ b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; diff --git a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart index c7ef3a9..2f5589c 100644 --- a/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart +++ b/lib/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart @@ -4,10 +4,8 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; class FacilitySelectionItem extends StatelessWidget { diff --git a/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart b/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart index cacc9d3..cb27f9f 100644 --- a/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart +++ b/lib/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart @@ -8,8 +8,6 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart' - show MyAppointmentsViewModel; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_selection_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart index ad48a6d..eed45df 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart @@ -1,58 +1,151 @@ import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/debouncer.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; +import '../../../../features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; +import '../../../emergency_services/call_ambulance/widgets/type_selection_widget.dart' show TypeSelectionWidget; + +// class HospitalBottomSheetBody extends StatelessWidget { +// late BookAppointmentsViewModel appointmentsViewModel; +// late AppointmentViaRegionViewmodel regionalViewModel; +// final TextEditingController searchText = TextEditingController(); +// +// HospitalBottomSheetBody({super.key}); +// +// @override +// Widget build(BuildContext context) { +// appointmentsViewModel = Provider.of(context); +// regionalViewModel = Provider.of(context); +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// LocaleKeys.selectHospital.tr(), +// style: TextStyle( +// fontSize: 21, +// fontWeight: FontWeight.w600, +// color: AppColors.blackColor, +// ), +// ), +// Text( +// LocaleKeys.selectHospitalSubTitle.tr(), +// style: TextStyle( +// fontSize: 16, +// fontWeight: FontWeight.w500, +// color: AppColors.greyTextColor, +// ), +// ), +// SizedBox(height: 16.h), +// TextInputWidget( +// labelText: LocaleKeys.search.tr(), +// hintText: LocaleKeys.searchHospital.tr(), +// controller: searchText, +// onChange: (value) { +// appointmentsViewModel.filterHospitalListByString( +// value, regionalViewModel.selectedRegionId, regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name); +// }, +// isEnable: true, +// prefix: null, +// autoFocus: false, +// isBorderAllowed: false, +// keyboardType: TextInputType.text, +// isAllowLeadingIcon: true, +// selectionType: SelectionTypeEnum.search, +// padding: EdgeInsets.symmetric( +// vertical: ResponsiveExtension(10).h, +// horizontal: ResponsiveExtension(15).h, +// ), +// ), +// SizedBox(height: 24.h), +// // TypeSelectionWidget( +// // hmcCount: "0", +// // hmgCount: "0", +// // ), +// // SizedBox(height: 21.h), +// SizedBox( +// height: MediaQuery.sizeOf(context).height * .4, +// child: ListView.separated( +// itemBuilder: (_, index) { +// var hospital = regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name +// ? appointmentsViewModel.filteredHospitalList!.registeredDoctorMap![regionalViewModel.selectedRegionId!]!.hmgDoctorList![index] +// : appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId!]?.hmcDoctorList?[index]; +// return HospitalListItem( +// hospitalData: hospital, +// isLocationEnabled: appointmentsViewModel.isLocationEnabled(), +// ).onPress(() { +// regionalViewModel.setHospitalModel(hospital); +// if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { +// regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); +// regionalViewModel.handleLastStepForRegion(); +// } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { +// regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); +// regionalViewModel.handleLastStepForClinic(); +// } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { +// regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); +// regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); +// // regionalViewModel.handleLastStepForClinic(); +// } +// }); +// }, +// separatorBuilder: (_, __) => SizedBox( +// height: 16.h, +// ), +// itemCount: (regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name +// ? (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId]?.hmgDoctorList) +// : (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId]?.hmcDoctorList)) +// ?.length ?? +// 0), +// ) +// ], +// ); +// } +// } + class HospitalBottomSheetBody extends StatelessWidget { - late BookAppointmentsViewModel appointmentsViewModel; - late AppointmentViaRegionViewmodel regionalViewModel; - final TextEditingController searchText = TextEditingController(); - HospitalBottomSheetBody({super.key}); + final TextEditingController searchText ; + final Debouncer debouncer = Debouncer(milliseconds: 500); + + final int hmcCount; + final int hmgCount; + final List? displayList; + final FacilitySelection selectedFacility; + final Function(FacilitySelection) onFacilityClicked; + final Function(PatientDoctorAppointmentList) onHospitalClicked; + final Function(String) onHospitalSearch; + + HospitalBottomSheetBody({super.key, required this.hmcCount, required this.hmgCount, this.displayList, required this.selectedFacility, required this.onFacilityClicked, required this.onHospitalClicked, required this.onHospitalSearch, required this.searchText}); @override Widget build(BuildContext context) { - appointmentsViewModel = Provider.of(context); - regionalViewModel = Provider.of(context); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - LocaleKeys.selectHospital.tr(), - style: TextStyle( - fontSize: 21, - fontWeight: FontWeight.w600, - color: AppColors.blackColor, - ), - ), - Text( - LocaleKeys.selectHospitalSubTitle.tr(), - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: AppColors.greyTextColor, - ), - ), - SizedBox(height: 16.h), TextInputWidget( labelText: LocaleKeys.search.tr(), hintText: LocaleKeys.searchHospital.tr(), controller: searchText, onChange: (value) { - appointmentsViewModel.filterHospitalListByString( - value, regionalViewModel.selectedRegionId, regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name); + debouncer.run((){ + onHospitalSearch(value??""); + }); }, isEnable: true, prefix: null, + autoFocus: false, isBorderAllowed: false, keyboardType: TextInputType.text, @@ -64,46 +157,34 @@ class HospitalBottomSheetBody extends StatelessWidget { ), ), SizedBox(height: 24.h), - // TypeSelectionWidget( - // hmcCount: "0", - // hmgCount: "0", - // ), - // SizedBox(height: 21.h), + TypeSelectionWidget( + selectedFacility:selectedFacility , + hmcCount: hmcCount.toString(), + hmgCount: hmgCount.toString(), + onitemClicked: (selectedValue){ + onFacilityClicked(selectedValue); + }, + ), + SizedBox(height: 21.h), SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: ListView.separated( - itemBuilder: (_, index) { - var hospital = regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name - ? appointmentsViewModel.filteredHospitalList!.registeredDoctorMap![regionalViewModel.selectedRegionId!]!.hmgDoctorList![index] - : appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId!]?.hmcDoctorList?[index]; + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) + { + var hospital = displayList?[index]; return HospitalListItem( hospitalData: hospital, - isLocationEnabled: appointmentsViewModel.isLocationEnabled(), + isLocationEnabled: true, ).onPress(() { - regionalViewModel.setHospitalModel(hospital); - if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); - regionalViewModel.handleLastStepForRegion(); - } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); - regionalViewModel.handleLastStepForClinic(); - } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); - regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); - // regionalViewModel.handleLastStepForClinic(); - } - }); - }, + onHospitalClicked(hospital!); + });}, separatorBuilder: (_, __) => SizedBox( - height: 16.h, - ), - itemCount: (regionalViewModel.selectedFacilityType == FacilitySelection.HMG.name - ? (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId]?.hmgDoctorList) - : (appointmentsViewModel.filteredHospitalList?.registeredDoctorMap?[regionalViewModel.selectedRegionId]?.hmcDoctorList)) - ?.length ?? - 0), - ) + height: 16.h, + ), + itemCount: displayList?.length ?? 0, + )) ], ); } } + diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart index 58a0d00..a2d22cd 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart @@ -1,21 +1,14 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; class HospitalBottomSheetBodyForDoctorFilter extends StatelessWidget { diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart index 660f95e..b01b541 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart @@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart index b023f7a..cbf68f6 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart @@ -4,7 +4,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart' show Consumer; diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart index 754db53..969e23a 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart @@ -5,7 +5,6 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index 6bc8007..6588c2e 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -6,7 +6,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart' show MyAppointmentsViewModel; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart' show RegionListItem; import 'package:provider/provider.dart'; @@ -80,7 +79,8 @@ class _RegionBottomSheetBodyState extends State { hmgCount: "${myAppointmentsVM.hospitalList?.registeredDoctorMap?[key]?.hmgSize ?? 0}", ).onPress(() { regionalViewModel.setSelectedRegionId(key); - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); + regionalViewModel.setDisplayListAndRegionHospitalList(myAppointmentsVM.hospitalList?.registeredDoctorMap![key]); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); }); }, ), diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 93b26c7..f0fd8cb 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; class SavedLogin extends StatefulWidget { @@ -266,9 +267,15 @@ class _SavedLogin extends State { child: CustomButton( text: LocaleKeys.guest.tr(), onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()), - ); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + // Navigator.of(context).pushAndRemoveUntil( + // MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()) + // ); }, backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), diff --git a/lib/presentation/blood_donation/blood_donation_page.dart b/lib/presentation/blood_donation/blood_donation_page.dart index 5f3fa86..a987220 100644 --- a/lib/presentation/blood_donation/blood_donation_page.dart +++ b/lib/presentation/blood_donation/blood_donation_page.dart @@ -7,16 +7,26 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/models/blood_group_hospitals_model.dart'; +import 'package:hmg_patient_app_new/features/blood_donation/widgets/hospital_selection.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_blood_group_widget.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_city_widget.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_gender_widget.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; class BloodDonationPage extends StatelessWidget { BloodDonationPage({super.key}); @@ -25,7 +35,7 @@ class BloodDonationPage extends StatelessWidget { @override Widget build(BuildContext context) { - appState = getIt.get(); + appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer(builder: (context, bloodDonationVM, child) { @@ -34,15 +44,83 @@ class BloodDonationPage extends StatelessWidget { Expanded( child: CollapsingListView( title: LocaleKeys.bloodDonation.tr(), + trailing: CustomButton( + text: "Book", + onPressed: () { + // if (bloodDonationVM.isUserAuthanticated()) { + bloodDonationVM.fetchHospitalsList().then((value) { + showCommonBottomSheetWithoutHeight(context, title: "Select Hospital", isDismissible: false, child: Consumer(builder: (_, data, __) { + return HospitalBottomSheetBodySelection( + onUserHospitalSelection: (BdGetProjectsHaveBdClinic userChoice) { + print("============User Choice==============="); + + bloodDonationVM.getFreeBloodDonationSlots(request: {"ClinicID": 134, "ProjectID": userChoice.projectId}); + }, + ); + }), callBackFunc: () {}); + }); + // } else { + // return showCommonBottomSheetWithoutHeight( + // context, + // title: LocaleKeys.notice.tr(context: context), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + // SizedBox(height: 8.h), + // (LocaleKeys.loginToUseService.tr()).toText16(color: AppColors.blackColor), + // SizedBox(height: 16.h), + // Row( + // children: [ + // Expanded( + // child: CustomButton( + // text: LocaleKeys.cancel.tr(), + // onPressed: () { + // Navigator.of(context).pop(); + // }, + // backgroundColor: AppColors.secondaryLightRedColor, + // borderColor: AppColors.secondaryLightRedColor, + // textColor: AppColors.primaryRedColor, + // icon: AppAssets.cancel, + // iconColor: AppColors.primaryRedColor, + // ), + // ), + // SizedBox(width: 8.h), + // Expanded( + // child: CustomButton( + // text: LocaleKeys.confirm.tr(), + // onPressed: () async { + // Navigator.of(context).pop(); + // // Navigator.pushAndRemoveUntil(context, CustomPageRoute(page: LandingNavigation()), (r) => false); + // await getIt().onLoginPressed(); + // }, + // backgroundColor: AppColors.bgGreenColor, + // borderColor: AppColors.bgGreenColor, + // textColor: Colors.white, + // icon: AppAssets.confirm, + // ), + // ), + // ], + // ), + // SizedBox(height: 16.h), + // ], + // ).center, + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + // } + }, + backgroundColor: AppColors.bgRedLightColor, + borderColor: AppColors.bgRedLightColor, + textColor: AppColors.primaryRedColor, + padding: EdgeInsetsGeometry.symmetric(vertical: 0.h, horizontal: 20.h)), child: Padding( padding: EdgeInsets.all(24.w), child: SingleChildScrollView( child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: false, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), child: Padding( padding: EdgeInsets.all(16.h), child: Column( @@ -60,8 +138,8 @@ class BloodDonationPage extends StatelessWidget { children: [ LocaleKeys.city.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), (appState.isArabic() - ? (bloodDonationVM.selectedCity.descriptionN ?? LocaleKeys.select.tr()) - : bloodDonationVM.selectedCity.description ?? LocaleKeys.select.tr(context: context)) + ? (bloodDonationVM.selectedCity?.descriptionN ?? LocaleKeys.select.tr()) + : bloodDonationVM.selectedCity?.description ?? LocaleKeys.select.tr(context: context)) .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), ], ), @@ -71,12 +149,7 @@ class BloodDonationPage extends StatelessWidget { ], ).onPress(() async { showCommonBottomSheetWithoutHeight(context, - title: LocaleKeys.selectCity.tr(context: context), - isDismissible: true, - child: SelectCityWidget( - bloodDonationViewModel: bloodDonationVM, - ), - callBackFunc: () {}); + title: LocaleKeys.selectCity.tr(context: context), isDismissible: true, child: SelectCityWidget(bloodDonationViewModel: bloodDonationVM), callBackFunc: () {}); }), SizedBox(height: 16.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), @@ -86,13 +159,16 @@ class BloodDonationPage extends StatelessWidget { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.genderInputIcon, width: 40.h, height: 40.h), SizedBox(width: 12.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.gender.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), - "Male".toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + (appState.isArabic() + ? (bloodDonationVM.selectedGender?.typeAr ?? LocaleKeys.select.tr()) + : bloodDonationVM.selectedGender?.type ?? LocaleKeys.select.tr(context: context)) + .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), ], ), ], @@ -103,10 +179,7 @@ class BloodDonationPage extends StatelessWidget { showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.selectGender.tr(context: context), isDismissible: true, - child: SelectGenderWidget( - isArabic: appState.isArabic(), - bloodDonationViewModel: bloodDonationVM, - ), + child: SelectGenderWidget(isArabic: appState.isArabic(), bloodDonationViewModel: bloodDonationVM), callBackFunc: () {}); }), SizedBox(height: 16.h), @@ -117,13 +190,17 @@ class BloodDonationPage extends StatelessWidget { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.bloodType, width: 40.h, height: 40.h), SizedBox(width: 12.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.bloodType.tr().toText16(color: AppColors.textColor, weight: FontWeight.w500), - bloodDonationVM.selectedBloodType.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + // bloodDonationVM.selectedBloodType?.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + (appState.isArabic() + ? (bloodDonationVM.selectedBloodType ?? LocaleKeys.select.tr()) + : bloodDonationVM.selectedBloodType ?? LocaleKeys.select.tr(context: context)) + .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500) ], ), ], @@ -134,10 +211,7 @@ class BloodDonationPage extends StatelessWidget { showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.select.tr(context: context), isDismissible: true, - child: SelectBloodGroupWidget( - isArabic: appState.isArabic(), - bloodDonationViewModel: bloodDonationVM, - ), + child: SelectBloodGroupWidget(isArabic: appState.isArabic(), bloodDonationViewModel: bloodDonationVM), callBackFunc: () {}); }), ], @@ -149,19 +223,73 @@ class BloodDonationPage extends StatelessWidget { ), ), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: SizedBox( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + GestureDetector( + onTap: bloodDonationVM.onTermAccepted, + child: Row( + children: [ + Selector( + selector: (_, viewModel) => viewModel.isTermsAccepted, + shouldRebuild: (previous, next) => previous != next, + builder: (context, isTermsAccepted, child) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 24.h, + width: 24.h, + decoration: BoxDecoration( + color: isTermsAccepted ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: isTermsAccepted ? AppColors.primaryRedBorderColor : AppColors.greyColor, width: 2.h), + ), + child: isTermsAccepted ? Icon(Icons.check, size: 16.f, color: Colors.white) : null, + ); + }, + ), + SizedBox(width: 12.h), + Row( + children: [ + Text( + LocaleKeys.iAcceptThe.tr(), + style: context.dynamicTextStyle(fontSize: 14.f, fontWeight: FontWeight.w500, color: Color(0xFF2E3039)), + ), + GestureDetector( + onTap: () { + // Navigate to terms and conditions page + Navigator.of(context).pushNamed('/terms'); + }, + child: Text( + LocaleKeys.termsConditoins.tr(), + style: context.dynamicTextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.primaryRedColor, + decoration: TextDecoration.underline, + decorationColor: AppColors.primaryRedBorderColor, + ), + ), + ), + ], + ), + // Expanded( + // child: Text( + // LocaleKeys.iAcceptTermsConditions.tr().split("the").first, + // style: context.dynamicTextStyle(fontSize: 14.fSize, fontWeight: FontWeight.w500, color: Color(0xFF2E3039)), + // ), + // ), + ], + ), + ).paddingOnly(left: 16.h, right: 16.h, top: 24.h), CustomButton( text: LocaleKeys.save.tr(), - onPressed: () { - // openDoctorScheduleCalendar(); + onPressed: () async { + DialogService dialogService = getIt.get(); + if (await bloodDonationVM.validateSelections()) { + bloodDonationVM.updateBloodGroup(); + } }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, diff --git a/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart b/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart index f9cadb2..a5ac071 100644 --- a/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart +++ b/lib/presentation/blood_donation/widgets/select_blood_group_widget.dart @@ -5,7 +5,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/city_list_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class SelectBloodGroupWidget extends StatelessWidget { diff --git a/lib/presentation/blood_donation/widgets/select_city_widget.dart b/lib/presentation/blood_donation/widgets/select_city_widget.dart index a0e8477..bf5992d 100644 --- a/lib/presentation/blood_donation/widgets/select_city_widget.dart +++ b/lib/presentation/blood_donation/widgets/select_city_widget.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/city_list_item.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; class SelectCityWidget extends StatelessWidget { SelectCityWidget({super.key, required this.bloodDonationViewModel}); @@ -28,9 +26,7 @@ class SelectCityWidget extends StatelessWidget { Navigator.of(context).pop(); }); }, - separatorBuilder: (_, __) => SizedBox( - height: 8.h, - ), + separatorBuilder: (_, __) => SizedBox(height: 8.h), itemCount: bloodDonationViewModel.citiesList.length), ) ], diff --git a/lib/presentation/blood_donation/widgets/select_gender_widget.dart b/lib/presentation/blood_donation/widgets/select_gender_widget.dart index b253331..0d360a8 100644 --- a/lib/presentation/blood_donation/widgets/select_gender_widget.dart +++ b/lib/presentation/blood_donation/widgets/select_gender_widget.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/city_list_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class SelectGenderWidget extends StatelessWidget { - SelectGenderWidget({super.key, required this.bloodDonationViewModel, required this.isArabic}); + const SelectGenderWidget({super.key, required this.bloodDonationViewModel, required this.isArabic}); - BloodDonationViewModel bloodDonationViewModel; - bool isArabic; + final BloodDonationViewModel bloodDonationViewModel; + final bool isArabic; @override Widget build(BuildContext context) { @@ -21,46 +21,45 @@ class SelectGenderWidget extends StatelessWidget { children: [ SizedBox(height: 8.h), SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: ListView.separated( - itemBuilder: (_, index) { - return DecoratedBox( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: false, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8.h, - children: [bloodDonationViewModel.genderList[index].name.toText16(color: AppColors.textColor, isBold: true)], + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [GenderTypeEnum.values[index].name.toCamelCase.toText16(color: AppColors.textColor, isBold: true)], + ), ), - ), - Transform.flip( - flipX: isArabic, - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, - iconColor: AppColors.blackColor, - width: 40.h, - height: 40.h, - fit: BoxFit.contain, + Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon, + iconColor: AppColors.blackColor, + width: 40.h, + height: 40.h, + fit: BoxFit.contain, + ), ), - ), - ], - ).paddingSymmetrical(16.h, 16.h).onPress(() { - // bloodDonationViewModel.setSelectedCity(bloodDonationViewModel.citiesList[index]); - Navigator.of(context).pop(); - })); - }, - separatorBuilder: (_, __) => SizedBox( - height: 8.h, - ), - itemCount: bloodDonationViewModel.genderList.length), - ) + ], + ).paddingSymmetrical(16.h, 16.h).onPress(() { + bloodDonationViewModel.onGenderChange(GenderTypeEnum.values[index].name.toCamelCase); + Navigator.of(context).pop(); + })); + }, + separatorBuilder: (_, __) => SizedBox( + height: 8.h, + ), + itemCount: GenderTypeEnum.values.length)) ], ); } diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 0e60a28..bcb2131 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -35,6 +35,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; +// import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; class BookAppointmentPage extends StatefulWidget { const BookAppointmentPage({super.key}); @@ -54,7 +55,7 @@ class _BookAppointmentPageState extends State { void initState() { authVM = context.read(); scheduleMicrotask(() { - bookAppointmentsViewModel.selectedTabIndex = 0; + // bookAppointmentsViewModel.selectedTabIndex = 0; bookAppointmentsViewModel.initBookAppointmentViewModel(); bookAppointmentsViewModel.getLocation(); immediateLiveCareViewModel.initImmediateLiveCare(); @@ -68,6 +69,7 @@ class _BookAppointmentPageState extends State { immediateLiveCareViewModel = Provider.of(context, listen: false); appState = getIt.get(); regionalViewModel = Provider.of(context, listen: true); + getSelectedTabData(bookAppointmentsViewModel.selectedTabIndex); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -85,6 +87,7 @@ class _BookAppointmentPageState extends State { CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + initialIndex: bookAppointmentsVM.selectedTabIndex, tabs: [ CustomTabBarModel(null, "General".needTranslation), CustomTabBarModel(null, "LiveCare".needTranslation), @@ -109,15 +112,13 @@ class _BookAppointmentPageState extends State { fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith") - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: true), + ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), ], ) : myAppointmentsVM.patientMyDoctorsList.isEmpty ? SizedBox() : Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (appState.isAuthenticated) ...[], "Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), @@ -143,7 +144,7 @@ class _BookAppointmentPageState extends State { children: [ Image.network( myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.w, + width: 64.h, height: 64.h, fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: false, radius: 50.r), @@ -237,10 +238,7 @@ class _BookAppointmentPageState extends State { ), ], ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsClinicsListLoading(true); @@ -272,10 +270,7 @@ class _BookAppointmentPageState extends State { ), ], ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setIsDoctorSearchByNameStarted(false); @@ -305,10 +300,7 @@ class _BookAppointmentPageState extends State { ), ], ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), ], ).onPress(() { bookAppointmentsViewModel.setProjectID(null); @@ -322,129 +314,123 @@ class _BookAppointmentPageState extends State { ).paddingSymmetrical(24.h, 0.h); case 1: //TODO: Get LiveCare type Select UI from Hussain - return appState.isAuthenticated - ? Column( - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Immediate Consultation".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500), - "Tap to select clinic".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), - ], - ), - ], - ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), - ], - ).onPress(() async { - //TODO Implement API to check for existing LiveCare Requests + return + // appState.isAuthenticated + // ? + Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Immediate Consultation".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500), + "Tap to select clinic".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + ], + ), + ], + ), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + ], + ).onPress(() async { + //TODO Implement API to check for existing LiveCare Requests - LoaderBottomSheet.showLoader(); - await immediateLiveCareViewModel.getPatientLiveCareHistory(); - LoaderBottomSheet.hideLoader(); + LoaderBottomSheet.showLoader(); + await immediateLiveCareViewModel.getPatientLiveCareHistory(); + LoaderBottomSheet.hideLoader(); - if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { - Navigator.of(context).push( - CustomPageRoute( - page: ImmediateLiveCarePendingRequestPage(), - ), - ); - } else { - Navigator.of(context).push( - CustomPageRoute( - page: SelectImmediateLiveCareClinicPage(), - ), - ); - } - }), - SizedBox(height: 16.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Scheduled Consultation".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500), - "Tap to select clinic".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), - ], - ), - ], - ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), - ], - ).onPress(() { - bookAppointmentsViewModel.setIsClinicsListLoading(true); - bookAppointmentsViewModel.setIsLiveCareSchedule(true); - Navigator.of(context).push( - CustomPageRoute( - page: SelectClinicPage(), - ), - ); - }), - SizedBox(height: 16.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_region_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Pharma LiveCare".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500), - "".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), - ], - ), - ], - ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), - ], - ).onPress(() { - openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); - }), - ], - ), - ), - ), - ], - ).paddingSymmetrical(24.h, 0.h) - : getLiveCareNotLoggedInUI(); + if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + Navigator.of(context).push( + CustomPageRoute( + page: SelectImmediateLiveCareClinicPage(), + ), + ); + } + }), + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Scheduled Consultation".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500), + "Tap to select clinic".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + ], + ), + ], + ), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + ], + ).onPress(() { + bookAppointmentsViewModel.setIsClinicsListLoading(true); + bookAppointmentsViewModel.setIsLiveCareSchedule(true); + Navigator.of(context).push( + CustomPageRoute( + page: SelectClinicPage(), + ), + ); + }), + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.search_by_region_icon, width: 40.h, height: 40.h), + SizedBox(width: 12.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Pharma LiveCare".needTranslation.toText14(color: AppColors.textColor, weight: FontWeight.w500), + "".needTranslation.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + ], + ), + ], + ), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), + ], + ).onPress(() { + openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); + }), + ], + ), + ), + ), + ], + ).paddingSymmetrical(24.h, 0.h) + // : getLiveCareNotLoggedInUI() + ; default: SizedBox.shrink(); } @@ -486,10 +472,8 @@ class _BookAppointmentPageState extends State { regionalViewModel.flush(); regionalViewModel.setBottomSheetType(type); // AppointmentViaRegionViewmodel? viewmodel = null; - showCommonBottomSheetWithoutHeight(context, - title: "", - titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), - isDismissible: false, child: Consumer(builder: (_, data, __) { + showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, + child: Consumer(builder: (_, data, __) { return getRegionalSelectionWidget(data); }), callBackFunc: () {}); } @@ -505,7 +489,34 @@ class _BookAppointmentPageState extends State { ); } if (data.bottomSheetState == AppointmentViaRegionState.HOSPITAL_SELECTION) { - return HospitalBottomSheetBody(); + return HospitalBottomSheetBody( + searchText: data.searchController, + displayList: data.displayList, + onFacilityClicked: (value) { + data.setSelectedFacility(value); + data.getDisplayList(); + }, + onHospitalClicked: (hospital) { + regionalViewModel.setHospitalModel(hospital); + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); + regionalViewModel.handleLastStepForRegion(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinic(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + var appointmentsViewModel = Provider.of(context); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); + } + }, + onHospitalSearch: (value) { + data.searchHospitals(value ?? ""); + }, + selectedFacility: data.selectedFacility, + hmcCount: data.hmcCount, + hmgCount: data.hmgCount, + ); } if (data.bottomSheetState == AppointmentViaRegionState.CLINIC_SELECTION) { // Navigator.of(context).pop(); @@ -548,9 +559,7 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Immediate service".needTranslation.toText18(color: AppColors.textColor, isBold: true), - "No need to wait, you will get medical consultation immediately via video call" - .needTranslation - .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + "No need to wait, you will get medical consultation immediately via video call".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), ], ), ), @@ -582,9 +591,7 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Doctor will contact".needTranslation.toText18(color: AppColors.textColor, isBold: true), - "A specialised doctor will contact you and will be able to view your medical history" - .needTranslation - .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + "A specialised doctor will contact you and will be able to view your medical history".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), ], ), ), @@ -600,9 +607,7 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Free medicine delivery".needTranslation.toText18(color: AppColors.textColor, isBold: true), - "Offers free medicine delivery for the LiveCare appointment" - .needTranslation - .toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + "Offers free medicine delivery for the LiveCare appointment".needTranslation.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), ], ), ), diff --git a/lib/presentation/book_appointment/doctor_filter/RegionChips.dart b/lib/presentation/book_appointment/doctor_filter/RegionChips.dart index c81e548..19e391e 100644 --- a/lib/presentation/book_appointment/doctor_filter/RegionChips.dart +++ b/lib/presentation/book_appointment/doctor_filter/RegionChips.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart' - show BookAppointmentsViewModel; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart b/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart index 155d8e3..982fc54 100644 --- a/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart +++ b/lib/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart @@ -2,26 +2,16 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/clinic_item.dart'; -import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/clinic_card.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; -import '../../../features/book_appointments/models/resp_models/get_clinic_list_response_model.dart' show GetClinicsListResponseModel; class ClinicBottomSheet extends StatelessWidget { late BookAppointmentsViewModel appointmentsViewModel; diff --git a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart index 6e51f8a..e4d11bd 100644 --- a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart +++ b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart @@ -9,7 +9,6 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body_for_doctor_filter.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/RegionChips.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_filter/clinic_bottomsheet.dart'; diff --git a/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart b/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart index f416ed4..e64c53f 100644 --- a/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart +++ b/lib/presentation/book_appointment/doctor_filter/facility_Chips.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart' - show BookAppointmentsViewModel; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index c3f4ad9..5ce7fac 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -1,18 +1,13 @@ -import 'dart:math'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/book_appointment/laser/laser_appointment.dart b/lib/presentation/book_appointment/laser/laser_appointment.dart index 440ae45..aae7990 100644 --- a/lib/presentation/book_appointment/laser/laser_appointment.dart +++ b/lib/presentation/book_appointment/laser/laser_appointment.dart @@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart' show CapExtension; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/laser_body_parts.dart'; @@ -82,7 +84,7 @@ class LaserAppointment extends StatelessWidget { activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null,LocaleKeys.malE.tr()), - CustomTabBarModel(null,LocaleKeys.female.tr()), + CustomTabBarModel(null, "Female".needTranslation), ], onTabChange: (index) { var viewmodel = context.read(); diff --git a/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart b/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart index d8fa736..baf72d2 100644 --- a/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart +++ b/lib/presentation/book_appointment/laser/widgets/body_type_listing.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/LaserCategoryType.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -45,20 +44,22 @@ class BodyTypeListing extends StatelessWidget { ? AppColors.chipPrimaryRedBorderColor : AppColors.blackColor, iconSize: 16, - labelPadding: - EdgeInsetsDirectional.only(start: 8.h, end: 0.h), - padding: - EdgeInsets.symmetric(vertical: 16.h, horizontal: 12.h), - deleteIconSize: Size(18.h, 18.h), - shape: SmoothRectangleBorder( - borderRadius: BorderRadius.circular(10), - smoothness: 10, + labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w), + padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 12.w), + deleteIconSize: Size(18.w, 18.h), + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + smoothness: 10, side: BorderSide( color: index == selectedBodyAreaIndex ? AppColors.chipPrimaryRedBorderColor : AppColors.borderGrayColor, width: 1), - )).onPress(() => onCategoryChanged(index)))) + )).onPress( + () => onCategoryChanged(index), + ), + ), + ) ], ), ); diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index 2877b27..2371f4a 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -1,3 +1,4 @@ + import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -322,15 +323,29 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { } Future askVideoCallPermission() async { - Map statuses = await [Permission.camera, Permission.microphone, Permission.notification].request(); + bool hasPermission = false; + if (Platform.isIOS) { + Map statuses = await [Permission.camera, Permission.microphone].request(); - if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted && statuses[Permission.notification] == PermissionStatus.granted) { - // Camera permission granted - return true; + if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { + // Camera permission granted + hasPermission = true; + } else { + hasPermission = false; + } } else { - return false; + Map statuses = await [Permission.camera, Permission.microphone, Permission.notification].request(); + + if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted && statuses[Permission.notification] == PermissionStatus.granted) { + // Camera permission granted + hasPermission = true; + } else { + hasPermission = false; + } } + return hasPermission; + // if (!(await Permission.camera.request().isGranted) || !(await Permission.microphone.request().isGranted)) { // return false; // } diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index b052969..48e79b1 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -11,30 +11,25 @@ import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart index 8c8b79c..8488a49 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -11,13 +10,11 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/countdown_timer.dart'; -import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart b/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart index 3913f10..cdf13c6 100644 --- a/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart +++ b/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart @@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; diff --git a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart index e4d69b1..31fb71f 100644 --- a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart +++ b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart @@ -6,10 +6,7 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class SelectLiveCareCallType extends StatelessWidget { diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index f76a5b0..72658bf 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -21,7 +21,6 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; -import 'package:hmg_patient_app_new/widgets/loading_dialog.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -51,146 +50,170 @@ class _ReviewAppointmentPageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.reviewAppointment.tr(context: context), - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24.h), - LocaleKeys.docInfo.tr(context: context).toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Image.network( - bookAppointmentsViewModel.selectedDoctor.doctorImageURL!, - width: 50.h, - height: 50.h, - fit: BoxFit.cover, - ).circle(100), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.49, - child: - "${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}".toString().toText16(isBold: true, maxlines: 1), - ), - Image.network( - bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!, - width: 20.h, - height: 15.h, - fit: BoxFit.cover, - ), - ], - ), - SizedBox(height: 2.h), - (bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "") - .toString() - .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1), - ], - ), - ], - ), - SizedBox(height: 12.h), - Wrap( - direction: Axis.horizontal, - spacing: 8.h, - runSpacing: 8.h, - children: [ - AppCustomChipWidget( - labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation, - ), - AppCustomChipWidget( - labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation, - ), - AppCustomChipWidget( - labelText: - "${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? DateUtil.formatDateToDate(DateTime.now(), false) : bookAppointmentsViewModel.selectedAppointmentDate}" - .needTranslation, - ), - AppCustomChipWidget( - labelText: - "${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? "Waiting Appointment".needTranslation : bookAppointmentsViewModel.selectedAppointmentTime}" - .needTranslation, - ), - ], - ), - ], + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + LocaleKeys.docInfo.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, ), - ), - ), - SizedBox(height: 24.h), - LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Image.asset( - appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, - width: 52.h, - height: 52.h, - ), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), - SizedBox(height: 8.h), - AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), - ], - ), - ], + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.network( + bookAppointmentsViewModel.selectedDoctor.doctorImageURL!, + width: 50.h, + height: 50.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.49, + child: "${bookAppointmentsViewModel.selectedDoctor.doctorTitle} ${bookAppointmentsViewModel.selectedDoctor.name}" + .toString() + .toText16(isBold: true, maxlines: 1), + ), + Image.network( + bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!, + width: 20.h, + height: 15.h, + fit: BoxFit.cover, + ), + ], + ), + SizedBox(height: 2.h), + (bookAppointmentsViewModel.selectedDoctor.speciality!.isNotEmpty ? bookAppointmentsViewModel.selectedDoctor.speciality!.first : "") + .toString() + .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, maxLine: 1), + ], + ), + ], + ), + SizedBox(height: 12.h), + Wrap( + direction: Axis.horizontal, + spacing: 8.h, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}".needTranslation, + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.branch.tr(context: context)} ${bookAppointmentsViewModel.selectedDoctor.projectName}".needTranslation, + ), + AppCustomChipWidget( + labelText: + "${LocaleKeys.date.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? DateUtil.formatDateToDate(DateTime.now(), false) : bookAppointmentsViewModel.selectedAppointmentDate}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "${LocaleKeys.time.tr(context: context)}: ${bookAppointmentsViewModel.isWaitingAppointmentSelected ? "Waiting Appointment".needTranslation : bookAppointmentsViewModel.selectedAppointmentTime}" + .needTranslation, + ), + ], + ), + ], + ), ), ), - ), - SizedBox(height: 24.h), - "Hospital Information".needTranslation.toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, + SizedBox(height: 24.h), + LocaleKeys.patientInfo.tr(context: context).toText16(isBold: true), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Image.asset( + appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, + width: 52.h, + height: 52.h, + ), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), + SizedBox(height: 8.h), + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + ], + ), + ], + ), + ), ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true), - ], + SizedBox(height: 24.h), + "Hospital Information".needTranslation.toText16(isBold: true), + SizedBox(height: 16.h), + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true), + SizedBox(height: 8.h), + bookAppointmentsViewModel.appointmentNearestGateResponseModel != null + ? Wrap( + direction: Axis.horizontal, + spacing: 8.w, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: bookAppointmentsVM.isAppointmentNearestGateLoading + ? "Floor: Ground Floor" + : "Floor: ${getIt.get().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}", + ).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + AppCustomChipWidget( + labelText: + "Nearest Gate: ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") + .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), + ], + ) + : SizedBox.shrink(), + ], + ), ), ), - ), - ], - ), - ), + ], + ), + ); + }), ), ), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, - borderRadius: 24.h, + borderRadius: 24.r, hasShadow: true, ), child: CustomButton( diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index bdb6773..d008497 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -2,8 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -21,7 +19,6 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import '../../features/book_appointments/models/resp_models/doctors_list_response_model.dart'; diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index d0bd740..15c8654 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -27,17 +28,19 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class SelectClinicPage extends StatefulWidget { bool isFromRegionFlow; + HealthCalculatorsTypeEnum? calculatorType; + dynamic calculatedResult; - SelectClinicPage({super.key, this.isFromRegionFlow = false}); + SelectClinicPage({super.key, this.isFromRegionFlow = false, this.calculatorType, this.calculatedResult}); @override State createState() => _SelectClinicPageState(); @@ -69,131 +72,873 @@ class _SelectClinicPageState extends State { bookAppointmentsViewModel = Provider.of(context, listen: false); regionalViewModel = Provider.of(context, listen: true); appState = getIt.get(); - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: bookAppointmentsViewModel.isLiveCareSchedule ? "Select LiveCare Clinic".needTranslation : LocaleKeys.selectClinic.tr(context: context), - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Consumer(builder: (context, bookAppointmentsVM, child) { - return Column( - children: [ - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.clinicName.tr(context: context), - controller: searchEditingController, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - focusNode: textFocusNode, - suffix: searchEditingController.text.isNotEmpty - ? GestureDetector( - onTap: () { - searchEditingController.clear(); - bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) - : null, - onChange: (value) { - bookAppointmentsViewModel.filterClinics(value!); - }, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, + if (widget.calculatorType != null) { + return CollapsingListView( + title: "Your ${widget.calculatorType!.name.toCamelCase}", + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r))), + padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.h), + child: CustomButton( + text: LocaleKeys.bookAppo.tr(), + onPressed: () { + if (widget.calculatedResult["clinicId"] == null) { + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + bookAppointmentsViewModel.isGetDocForHealthCal = true; + bookAppointmentsViewModel.calculationID = widget.calculatedResult["calculationID"]; + Navigator.push(context, CustomPageRoute(page: SelectDoctorPage())); + } else { + onClinicSelected(GetClinicsListResponseModel(clinicID: widget.calculatedResult["clinicId"], clinicDescription: "asdfds", clinicDescriptionN: "asdfds")); + } + }, + icon: null, + fontSize: 16.f, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + borderRadius: 12.r, + fontWeight: FontWeight.w500), + ), + child: getCalculatorResultWidget(type: widget.calculatorType!, calculatedResult: widget.calculatedResult).paddingSymmetrical(18.w, 24.h), + ); + } else { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: bookAppointmentsViewModel.isLiveCareSchedule ? "Select LiveCare Clinic".needTranslation : LocaleKeys.selectClinic.tr(context: context), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return Column( + children: [ + SizedBox(height: 16.h), + TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.clinicName.tr(context: context), + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + focusNode: textFocusNode, + suffix: searchEditingController.text.isNotEmpty + ? GestureDetector( + onTap: () { + searchEditingController.clear(); + bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + bookAppointmentsViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), ), - ), - bookAppointmentsVM.isLiveCareSchedule - ? ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, - itemBuilder: (context, index) { - return bookAppointmentsVM.isClinicsListLoading - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); - }), + bookAppointmentsVM.isLiveCareSchedule + ? ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isClinicsListLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); + }), + ), ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ) - : ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, - itemBuilder: (context, index) { - return bookAppointmentsVM.isClinicsListLoading - ? ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), - clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index], - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]); - }), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ) + : ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, + itemBuilder: (context, index) { + return bookAppointmentsVM.isClinicsListLoading + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: GetLiveCareClinicsResponseModel(), + clinicsListResponseModel: bookAppointmentsVM.filteredClinicsList[index], + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onClinicSelected(bookAppointmentsVM.filteredClinicsList[index]); + }), + ), ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + ], + ); + }), + ), + ), + ), + ); + } + } + + Widget getCalculatorResultWidget({required HealthCalculatorsTypeEnum type, dynamic calculatedResult}) { + switch (widget.calculatorType!) { + case HealthCalculatorsTypeEnum.bmi: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.calories: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bmr: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.idealBodyWeight: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bodyFat: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.crabsProteinFat: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.ovulation: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.deliveryDueDate: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bloodSugar: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.bloodCholesterol: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + case HealthCalculatorsTypeEnum.triglycerides: + return resultWidget(type: widget.calculatorType!, calculatedResult: calculatedResult); + } + } + + Widget resultWidget({required HealthCalculatorsTypeEnum type, dynamic calculatedResult}) { + return Column( + children: [ + if (type == HealthCalculatorsTypeEnum.bmi) ...[ + if (calculatedResult["bmiCategory"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["bmiCategory"], + calories: calculatedResult["bmiResult"], + description: + 'Your BMI is within the healthy range. Continue a balanced diet, regular physical activity, and routine checkups to maintain good health. Monitor any significant changes over time.', + color: Colors.green, + icon: Icons.scale, + hide: true, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmiCategory"] == "Underweight") ...[ + calorieWidget( + title: calculatedResult["bmiCategory"], + calories: calculatedResult["bmiResult"], + description: + 'BMI suggests you are underweight. Consider increasing calorie intake with nutrient-dense foods, include resistance training to build muscle, and consult a healthcare professional to check for underlying causes.', + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmiCategory"] == "Overweight") ...[ + calorieWidget( + title: calculatedResult["bmiCategory"], + calories: calculatedResult["bmiResult"], + description: + 'BMI indicates overweight. Aim for gradual, sustainable weight loss through a modest calorie deficit, regular physical activity, and healthier food choices. Seek medical advice before major lifestyle changes.', + color: Colors.red, + hide: true, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.calories) ...[ + if (calculatedResult["calorieRange"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["calorieRange"], + calories: calculatedResult["calories"], + description: + 'Estimated daily calories to maintain your current weight given your activity level and metabolism. Use this as a baseline: increase slightly for muscle gain, or create a small deficit for gradual weight loss. Track progress and adjust every 2–4 weeks.', + color: Colors.green, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["calorieRange"] == "Low") ...[ + calorieWidget( + title: calculatedResult["calorieRange"], + calories: calculatedResult["calories"], + description: + 'A moderate calorie reduction designed for steady, sustainable weight loss (typically 0.25–0.5 kg per week). Combine with resistance training and adequate protein to preserve muscle. Avoid extreme cuts and consult a professional if you have medical conditions.', + color: Colors.orange, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["calorieRange"] == "High") ...[ + calorieWidget( + title: calculatedResult["calorieRange"], + calories: calculatedResult["calories"], + description: + 'A large calorie reduction that may produce faster weight loss but can be hard to sustain and increase risk of nutrient deficiencies, fatigue, or muscle loss. Not recommended long-term; seek guidance from a healthcare or nutrition professional before continuing.', + color: Colors.red, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.bmr) ...[ + if (calculatedResult["bmrRange"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["bmrRange"], + calories: calculatedResult["bmr"], + description: + 'Your Basal Metabolic Rate (BMR) is within the expected range for your profile. BMR is the energy your body needs at rest to maintain vital functions (breathing, circulation, temperature). Use this value as the baseline for estimating total daily energy needs by applying an activity multiplier. Maintain a balanced diet and regular physical activity to support metabolic health.', + color: Colors.green, + hide: true, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmrRange"] == "Low") ...[ + calorieWidget( + title: calculatedResult["bmrRange"], + calories: calculatedResult["bmr"], + description: + 'Your BMR is lower than typical for your profile. Possible causes include lower lean muscle mass, age-related metabolic decline, or metabolic adaptation from prolonged calorie restriction. Consider focusing on resistance training to build/maintain muscle, ensure adequate protein and micronutrient intake, and consult a healthcare professional if this is unexpected.', + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["bmrRange"] == "High") ...[ + calorieWidget( + title: calculatedResult["bmrRange"], + calories: calculatedResult["bmr"], + description: + 'Your BMR is higher than average for your profile. This can reflect greater lean mass, younger age, or a naturally higher metabolic rate, meaning you burn more energy at rest. Use this information to tailor calorie targets: higher needs for maintenance or muscle gain, moderate deficit for weight loss. Seek medical advice if you notice rapid unexplained changes.', + color: Colors.red, + hide: true, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.idealBodyWeight) ...[ + if (calculatedResult["status"] == "Normal") ...[ + calorieWidget( + title: calculatedResult["status"], + calories: calculatedResult["ibw"], + hide: true, + description: (() { + final diff = calculatedResult["difference"]; + if (diff is num) { + if (diff == 0) { + return 'Your weight is at the ideal target. Maintain your current habits — balanced diet and regular activity — to keep this.'; + } else if (diff > 0) { + return 'You are below the ideal weight by ${diff.toStringAsFixed(1)} kg. Aim to gradually increase weight with a modest calorie surplus, nutrient-dense foods, and resistance training.'; + } else { + return 'You are above the ideal weight by ${diff.abs().toStringAsFixed(1)} kg. Aim for a gradual, sustainable weight reduction through a moderate calorie deficit, increased activity, and balanced nutrition.'; + } + } + return 'Your weight is close to the ideal range. Maintain a balanced diet and regular activity; follow personalized advice from a healthcare professional if needed.'; + })(), + color: Colors.green, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["status"] == "Underweight") ...[ + calorieWidget( + title: calculatedResult["status"], + calories: calculatedResult["ibw"], + hide: true, + description: (() { + final diff = calculatedResult["difference"]; + if (diff is num) { + return 'You are below the ideal weight by ${diff.toStringAsFixed(1)} kg. Focus on increasing calorie intake with nutrient-dense foods, prioritize protein and resistance training to build healthy mass, and consider consulting a healthcare professional.'; + } + return 'You appear underweight compared to the ideal. Consider increasing calorie intake with nutrient-dense foods and resistance training; seek professional guidance if needed.'; + })(), + color: Colors.orange, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["status"] == "Overweight") ...[ + calorieWidget( + title: calculatedResult["status"], + calories: calculatedResult["ibw"], + hide: true, + description: (() { + final diff = calculatedResult["difference"]; + if (diff is num) { + return 'You are above the ideal weight by ${diff.abs().toStringAsFixed(1)} kg. Aim for a gradual, sustainable weight loss strategy — moderate calorie deficit, regular physical activity, and balanced nutrition. Consult a professional before major changes.'; + } + return 'You appear above the ideal weight. Consider a gradual, sustainable calorie deficit combined with activity and balanced nutrition; consult a professional if needed.'; + })(), + color: Colors.red, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.crabsProteinFat) ...[ + if (calculatedResult["dietType"] == "Very Low Crabs") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Very low‑carb (ketogenic) approach: minimal carbohydrates, higher fat and moderate protein. May support rapid weight loss and improved blood sugar control for some, but can be hard to sustain and may cause nutrient gaps. Monitor hydration/electrolytes and consult a professional for long‑term use.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown: Carbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "Low Crabs") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Low‑carb, higher‑protein plan: reduced carbohydrates with increased protein to support satiety and muscle maintenance. Helpful for weight management and glycemic control when balanced with vegetables and healthy fats. Ensure adequate fiber and micronutrients.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.orange, + hide: true, + icon: Icons.trending_down, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "Moderate Crabs") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Moderate carbohydrate intake with balanced protein and fats: provides steady energy for daily activity and exercise while allowing flexibility. Suitable for many people aiming for sustainable weight management and performance.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.green, + hide: true, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "USDA Guidelines ") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'USDA‑based balanced pattern: emphasizes fruits, vegetables, whole grains, lean proteins, and limited added sugars and saturated fats. Evidence‑based framework for general health, nutrient adequacy, and chronic disease prevention.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.green, + hide: true, + icon: Icons.scale, + ), + SizedBox(height: 16.h), + ] else if (calculatedResult["dietType"] == "Zone Diet") ...[ + calorieWidget( + title: calculatedResult["dietType"].toString(), + calories: calculatedResult["totalCalories"], + description: (() { + final total = calculatedResult["totalCalories"]; + final carbsG = calculatedResult["carbsGrams"]; + final carbsC = calculatedResult["carbsCalories"]; + final protG = calculatedResult["proteinGrams"]; + final protC = calculatedResult["proteinCalories"]; + final fatG = calculatedResult["fatGrams"]; + final fatC = calculatedResult["fatCalories"]; + final base = + 'Zone Diet (~40% carbs / 30% protein / 30% fat): emphasizes hormonal balance and portion control. May improve body composition and energy for some, but requires planning; personalize with a nutrition professional.'; + // if (total is num) { + // return '$base\n\nTotal: ${total.toStringAsFixed(0)} kcal.\n\nBreakdown:\nCarbs ${carbsG is num ? carbsG.toStringAsFixed(0) + " g" : "N/A"} (${carbsC is num ? carbsC.toStringAsFixed(0) + " kcal" : "N/A"}),\nProtein ${protG is num ? protG.toStringAsFixed(0) + " g" : "N/A"} (${protC is num ? protC.toStringAsFixed(0) + " kcal" : "N/A"}),\nFat ${fatG is num ? fatG.toStringAsFixed(1) + " g" : "N/A"} (${fatC is num ? fatC.toStringAsFixed(0) + " kcal" : "N/A"}).'; + // } + return base; + })(), + chipsItems: [ + ('Carbohydrates: ' + calculatedResult["carbsGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["carbsCalories"].toStringAsFixed(0) + ' kcal'), + ('Protein: ' + calculatedResult["proteinGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["proteinCalories"].toStringAsFixed(0) + ' kcal'), + ('Fat: ' + calculatedResult["fatGrams"].toStringAsFixed(0) + 'g = ' + calculatedResult["fatCalories"].toStringAsFixed(0) + ' kcal'), + ], + color: Colors.orange, + hide: true, + icon: Icons.warning_rounded, + ), + ] + ] else if (type == HealthCalculatorsTypeEnum.ovulation) ...[ + Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.nextPeriodAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["lastPeriodDate"].toString().toText16(color: AppColors.successColor, weight: FontWeight.w600), + ], ), - ], - ); - }), + ], + ), + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["lastPeriodDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + SizedBox(height: 16.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.ovulationAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["ovulationDate"].toString().toText16(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["ovulationDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + SizedBox(height: 16.h), + "Fertility Window".toText16(color: AppColors.textColor, weight: FontWeight.w600), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.fertileAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["fertileWindowStart"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["fertileWindowEnd"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + "This is your fertile window. Maintaining a healthy calorie range supports your body during this period, considering your Basal Metabolic Rate and activity. Your Body Mass Index is within the healthy range." + .toText12(height: 1.5, color: AppColors.textColorLight, fontWeight: FontWeight.w500) + ], + ), + ), + SizedBox( + height: 16.h, + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bulb), + SizedBox( + width: 8.w, + ), + Row( + children: [ + "Tips to keep in mind".toText16(color: AppColors.textColor, weight: FontWeight.w600), + ], + ), + ], + ), + SizedBox(height: 8.h), + Column( + children: [ + _buildTipRow("Drink before you feel thirsty."), + SizedBox(height: 4.h), + _buildTipRow("Keep a refillable bottle next to you."), + SizedBox(height: 4.h), + _buildTipRow("Track your daily intake to stay motivated."), + SizedBox(height: 4.h), + _buildTipRow("Choose sparkling water instead of soda."), + ], + ).paddingOnly(left: 4.w, right: 4.w) + ], + ), + ), + ], + ) + ] else if (type == HealthCalculatorsTypeEnum.deliveryDueDate) ...[ + Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDueDateAccordion, width: 30.h, height: 30.h), + SizedBox(width: 8.w), + calculatedResult["dueDate"].toString().toText16(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + Expanded(child: SizedBox(width: 8.w)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: calculatedResult["dueDateDay"].toString(), + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + ], + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "First Trimester".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Expanded(child: SizedBox(width: 8.h)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["firstTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.fertileAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["firstTrimester"]["start"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["firstTrimester"]["end"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Second Trimester".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Expanded(child: SizedBox(width: 8.h)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["secondTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.ovulationAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["secondTrimester"]["start"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["secondTrimester"]["end"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Third Trimester".toText16(color: AppColors.textColor, weight: FontWeight.w600), + Expanded(child: SizedBox(width: 8.h)), + CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: "${calculatedResult["thirdTrimester"]["weeks"]} Weeks", + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.pregnancyDayAccordion, width: 30.h, height: 30.h), + SizedBox( + width: 8.w, + ), + calculatedResult["thirdTrimester"]["start"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + SizedBox( + width: 4.w, + ), + "-".toText16(isBold: true, color: AppColors.successColor), + SizedBox( + width: 4.w, + ), + calculatedResult["thirdTrimester"]["end"].toString().toText13(color: AppColors.successColor, weight: FontWeight.w600), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + ], + ) + ], + ], + ); + } + + Widget _buildTipRow(String text) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 6.h, right: 8.w), + child: CircleAvatar( + radius: 3.r, + backgroundColor: AppColors.textColorLight, ), ), + Expanded( + child: text.toText12(color: AppColors.textColorLight, fontWeight: FontWeight.w500, height: 1.2.h), + ), + ], + ); + } + + Widget calorieWidget({required String title, required dynamic calories, required String description, required Color color, required IconData icon, bool hide = false, List? chipsItems}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ('${calories.toStringAsFixed(0)}').toText32(color: getTextColor(title), isBold: true), + SizedBox(width: 10.w), + hide ? SizedBox() : 'kcal/day'.toText12(color: AppColors.inputLabelTextColor, isBold: true, fontWeight: FontWeight.w600) + ], + ), + _buildStatusIcon(title) + ], + ), + SizedBox(height: 16.h), + if (chipsItems != null && chipsItems.isNotEmpty) ...[ + Wrap( + spacing: 8.0, // Horizontal space between chips + runSpacing: 4.0, // Vertical space between rows of chips + children: chipsItems.map((item) { + return CustomChipWidget( + height: 30.h, + chipType: ChipTypeEnum.alert, + backgroundColor: AppColors.lightGrayBGColor, + chipText: item, + iconAsset: null, + isShowBorder: false, + borderRadius: 8.h, + textColor: AppColors.textColor) + .paddingOnly(bottom: 4.h); + }).toList(), + ), + SizedBox(height: 8.h), + ], + title.toText16(color: AppColors.textColor, weight: FontWeight.w600), + SizedBox(height: 8.h), + description.toText12(height: 1.2.h, color: AppColors.textColorLight, fontWeight: FontWeight.w500) + ], ), ); } + Widget _buildStatusIcon(String title) { + final key = title.trim(); + if (key == 'Maintenance Calories' || key == 'Normal' || key == 'Ideal Weight' || key == 'Moderate Crabs' || key == 'USDA Guidelines') { + return Utils.buildSvgWithAssets(icon: AppAssets.checkmark_icon, width: 24.w, height: 24.h); + } else if (key == 'Moderate Deficit' || key == 'Low' || key == 'Underweight' || key == 'Very Low Crabs' || key == 'Low Crabs' || key == 'Zone Diet') { + return Utils.buildSvgWithAssets(icon: AppAssets.trade_down_yellow, width: 24.w, height: 24.h); + } else if (key == 'Aggressive Deficit' || key == 'High' || key == 'Overweight' || key == "Obese") { + return Utils.buildSvgWithAssets(icon: AppAssets.trade_down_red, width: 24.w, height: 24.h); + } else { + return SizedBox(); + } + } + + Color getTextColor(String title) { + final key = title.trim(); + if (key == 'Maintenance Calories' || key == 'Normal' || key == 'Ideal Weight' || key == 'Moderate Crabs' || key == 'USDA Guidelines') { + return AppColors.successColor; + } else if (key == 'Moderate Deficit' || key == 'Low' || key == 'Underweight' || key == 'Very Low Crabs' || key == 'Low Crabs' || key == 'Zone Diet') { + return AppColors.warningColor; + } else if (key == 'Aggressive Deficit' || key == 'High' || key == 'Overweight' || key == "Obese") { + return AppColors.errorColor; + } else { + return Colors.black; + } + } + void onLiveCareClinicSelected(GetLiveCareClinicsResponseModel clinic) { bookAppointmentsViewModel.setLiveCareSelectedClinic(clinic); bookAppointmentsViewModel.setIsDoctorsListLoading(true); @@ -210,9 +955,12 @@ class _SelectClinicPageState extends State { if (clinic.isLiveCareClinicAndOnline ?? false) { Navigator.of(context).push( CustomPageRoute( - page: SelectLivecareClinicPage(onNegativeClicked: (){ + page: SelectLivecareClinicPage( + onNegativeClicked: () { handleDoctorScreen(clinic); - },), + }, + selectedClinic: clinic, + ), ), ); } else { @@ -226,9 +974,7 @@ class _SelectClinicPageState extends State { //Dental Clinic Flow if (clinic.clinicID == 17) { if (appState.isAuthenticated) { - initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel - .currentlySelectedHospitalFromRegionFlow ?? - "0")); + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); return; } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); @@ -239,8 +985,7 @@ class _SelectClinicPageState extends State { ); return; } - } - else if (clinic.clinicID == 253) { + } else if (clinic.clinicID == 253) { bookAppointmentsViewModel.resetLaserData(); bookAppointmentsViewModel.getLaserClinic(); Navigator.push( @@ -267,21 +1012,19 @@ class _SelectClinicPageState extends State { } } - void openRegionListBottomSheet( - BuildContext context, RegionBottomSheetType type) { + void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) { bookAppointmentsViewModel.setProjectID(null); regionalViewModel.flush(); regionalViewModel.setBottomSheetType(type); // AppointmentViaRegionViewmodel? viewmodel = null; showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, - child: Consumer(builder: (_, data, __) { - return getRegionalSelectionWidget(data); - }), callBackFunc: () { - }); + child: Consumer(builder: (context, data, __) { + return getRegionalSelectionWidget(data, context); + }), callBackFunc: () {}); } - Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data) { + Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data, BuildContext context) { if (data.bottomSheetState == AppointmentViaRegionState.REGION_SELECTION) { return RegionBottomSheetBody(); } @@ -292,7 +1035,34 @@ class _SelectClinicPageState extends State { ); } if (data.bottomSheetState == AppointmentViaRegionState.HOSPITAL_SELECTION) { - return HospitalBottomSheetBody(); + return HospitalBottomSheetBody( + searchText: data.searchController, + displayList: data.displayList, + onFacilityClicked: (value) { + data.setSelectedFacility(value); + data.getDisplayList(); + }, + onHospitalClicked: (hospital) { + regionalViewModel.setHospitalModel(hospital); + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.CLINIC_SELECTION); + regionalViewModel.handleLastStepForRegion(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinic(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + var appointmentsViewModel = Provider.of(context, listen: false); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.DOCTOR_SELECTION); + regionalViewModel.handleLastStepForClinicForDentalAndLaser(appointmentsViewModel.selectedClinic.clinicID ?? -1); + } + }, + onHospitalSearch: (value) { + data.searchHospitals(value ?? ""); + }, + selectedFacility: data.selectedFacility, + hmcCount: data.hmcCount, + hmgCount: data.hmgCount, + ); } if (data.bottomSheetState == AppointmentViaRegionState.DOCTOR_SELECTION) { //if the region screen is opened for the dental clinic then the project id will be in the hospital list as the list is formed form the get project api @@ -306,7 +1076,7 @@ class _SelectClinicPageState extends State { if (appState.isAuthenticated) { initDentalAppointment(); return SizedBox.shrink(); - }else { + } else { bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); } } @@ -319,8 +1089,7 @@ class _SelectClinicPageState extends State { } bookAppointmentsViewModel.setProjectID(id); return SizedBox.shrink(); - } - else { + } else { return SizedBox.shrink(); } return SizedBox.shrink(); @@ -442,9 +1211,7 @@ class _SelectClinicPageState extends State { bookAppointmentsViewModel.setIsContinueDentalPlan(true); Navigator.of(context).pop(); Navigator.of(context).push( - CustomPageRoute( - page: SelectDoctorPage(), - ), + CustomPageRoute(page: SelectDoctorPage()), ); }, backgroundColor: AppColors.bgGreenColor, @@ -474,9 +1241,7 @@ class _SelectClinicPageState extends State { void initDentalAppointment() async { await Future.delayed(Duration(milliseconds: 300)); - initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel - .currentlySelectedHospitalFromRegionFlow ?? - "0")); + initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); return; } } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 46c3774..569e8ac 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; @@ -32,20 +33,28 @@ class SelectDoctorPage extends StatefulWidget { class _SelectDoctorPageState extends State { TextEditingController searchEditingController = TextEditingController(); + int? expandedIndex; FocusNode textFocusNode = FocusNode(); late AppState appState; late BookAppointmentsViewModel bookAppointmentsViewModel; + late ScrollController _scrollController; + final Map _itemKeys = {}; + @override void initState() { + _scrollController = ScrollController(); scheduleMicrotask(() { + bookAppointmentsViewModel.setIsNearestAppointmentSelected(false); if (bookAppointmentsViewModel.isLiveCareSchedule) { bookAppointmentsViewModel.getLiveCareDoctorsList(); } else { if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { bookAppointmentsViewModel.getDentalChiefComplaintDoctorsList(); + } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { + bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { bookAppointmentsViewModel.getDoctorsList(); } @@ -54,6 +63,12 @@ class _SelectDoctorPageState extends State { super.initState(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { bookAppointmentsViewModel = Provider.of(context, listen: false); @@ -62,7 +77,22 @@ class _SelectDoctorPageState extends State { backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: "Choose Doctor".needTranslation, + // bottomChild: Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r))), + // padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.h), + // child: CustomButton( + // text: LocaleKeys.search.tr(), + // onPressed: () { + // }, + // icon: null, + // fontSize: 16.f, + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // borderRadius: 12.r, + // fontWeight: FontWeight.w500), + // ), child: SingleChildScrollView( + controller: _scrollController, child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.h), child: Consumer(builder: (context, bookAppointmentsVM, child) { @@ -105,23 +135,82 @@ class _SelectDoctorPageState extends State { ), ], ), + SizedBox(height: 16.h), + if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + bookAppointmentsVM.setIsSortByClinic(true); + }, + backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + textColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + bookAppointmentsVM.setIsSortByClinic(false); + }, + backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.textColor.withOpacity(0.2) : AppColors.primaryRedColor, + textColor: bookAppointmentsVM.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(0.h, 0.h), + if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) + SizedBox(height: 16.h), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.nearestAppo.tr(context: context).toText13(isBold: true), + SizedBox(height: 4.h), + "View nearest available appointments".needTranslation.toText11(color: AppColors.textColorLight, weight: FontWeight.w500), + ], + ), + const Spacer(), + Switch( + activeThumbColor: AppColors.successColor, + activeTrackColor: AppColors.successColor.withValues(alpha: .15), + value: bookAppointmentsVM.isNearestAppointmentSelected, + onChanged: (newValue) async { + bookAppointmentsVM.setIsNearestAppointmentSelected(newValue); + + }, + ), + ], + ), ListView.separated( - padding: EdgeInsets.only(top: 24.h), + padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 - : (bookAppointmentsVM.isLiveCareSchedule - ? (bookAppointmentsVM.liveCareDoctorsList.isNotEmpty ? bookAppointmentsVM.liveCareDoctorsList.length : 1) - : (bookAppointmentsVM.doctorsList.isNotEmpty ? bookAppointmentsVM.doctorsList.length : 1)), + : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1), itemBuilder: (context, index) { + final isExpanded = expandedIndex == index; return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( doctorsListResponseModel: DoctorsListResponseModel(), isLoading: true, bookAppointmentsViewModel: bookAppointmentsViewModel, ) - : checkIsDoctorsListEmpty() + : bookAppointmentsVM.doctorsListGrouped.isEmpty ? Utils.getNoDataWidget(context, noDataText: "No Doctor found for selected criteria...".needTranslation) : AnimationConfiguration.staggeredList( position: index, @@ -130,40 +219,107 @@ class _SelectDoctorPageState extends State { verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( + key: _itemKeys.putIfAbsent(index, () => GlobalKey()), duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: - RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: DoctorCard( - doctorsListResponseModel: bookAppointmentsVM.isLiveCareSchedule - ? bookAppointmentsVM.liveCareDoctorsList[index] - : bookAppointmentsVM.doctorsList[index], - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.isLiveCareSchedule - ? bookAppointmentsVM.liveCareDoctorsList[index] - : bookAppointmentsVM.doctorsList[index]); - // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + // After rebuild, ensure the expanded item is visible + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _itemKeys[index]; + if (key != null && key.currentContext != null && expandedIndex == index) { + Scrollable.ensureVisible( + key.currentContext!, + duration: Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } + }); + }, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row with count badge and expand/collapse icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${bookAppointmentsVM.doctorsListGrouped[index].length} ${'doctors'.needTranslation}", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + // Clinic/Hospital name as group title + Text( + bookAppointmentsVM.isSortByClinic + ? (bookAppointmentsVM.doctorsListGrouped[index].first.clinicName ?? 'Unknown') + : (bookAppointmentsVM.doctorsListGrouped[index].first.projectName ?? 'Unknown'), + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + // Expanded content - list of doctors in this group + AnimatedSwitcher( + duration: Duration(milliseconds: 400), + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.only(top: 12.h), + child: Column( + children: bookAppointmentsVM.doctorsListGrouped[index].map((doctor) { + return Container( + margin: EdgeInsets.only(bottom: 12.h), + child: DoctorCard( + doctorsListResponseModel: doctor, + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ); + }).toList(), + ), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ), ), ), ), diff --git a/lib/presentation/book_appointment/select_livecare_clinic_page.dart b/lib/presentation/book_appointment/select_livecare_clinic_page.dart index 719452d..502e38d 100644 --- a/lib/presentation/book_appointment/select_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/select_livecare_clinic_page.dart @@ -1,22 +1,37 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; +import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; class SelectLivecareClinicPage extends StatelessWidget { final VoidCallback? onNegativeClicked; - const SelectLivecareClinicPage({super.key, this.onNegativeClicked}); + late GetClinicsListResponseModel selectedClinic; + + SelectLivecareClinicPage({super.key, this.onNegativeClicked, required this.selectedClinic}); + + late ImmediateLiveCareViewModel immediateLiveCareViewModel; @override Widget build(BuildContext context) { + immediateLiveCareViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -33,7 +48,7 @@ class SelectLivecareClinicPage extends StatelessWidget { SizedBox(height: 40.h), Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.h, height: 58.h), + Utils.buildSvgWithAssets(icon: AppAssets.immediate_service_icon, width: 58.w, height: 58.h), SizedBox(width: 18.h), Expanded( child: Column( @@ -103,7 +118,36 @@ class SelectLivecareClinicPage extends StatelessWidget { children: [ CustomButton( text: "Yes please, I am in a hurry".needTranslation, - onPressed: () {}, + onPressed: () async { + Navigator.pop(context); + GetLiveCareClinicListResponseModel liveCareClinic = GetLiveCareClinicListResponseModel( + iD: selectedClinic.liveCareClinicID, + serviceID: selectedClinic.liveCareServiceID, + serviceName: selectedClinic.clinicDescription, + serviceNameN: selectedClinic.clinicDescriptionN, + ); + + immediateLiveCareViewModel.setLiveCareSelectedCallType(1); + immediateLiveCareViewModel.setImmediateLiveCareSelectedClinic(liveCareClinic); + LoaderBottomSheet.showLoader(loadingText: "Fetching fees, Please wait...".needTranslation); + await immediateLiveCareViewModel.getLiveCareImmediateAppointmentFees(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(getIt.get().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ImmediateLiveCarePaymentDetails(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, diff --git a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart index 965844a..4a2a304 100644 --- a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart +++ b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_online_checkin_sheet.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -143,6 +142,7 @@ class WaitingAppointmentOnlineCheckinSheet extends StatelessWidget { bookAppointmentsViewModel.waitingAppointmentProjectID, onSuccess: (value) { LoaderBottomSheet.hideLoader(); + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.waitingAppointmentProjectID, clinicID: bookAppointmentsViewModel.waitingAppointmentDoctor!.clinicID!); bookAppointmentsViewModel.setIsWaitingAppointmentSelected(true); Navigator.of(context).push( CustomPageRoute( diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index 7c4226c..3a9f57d 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; @@ -21,7 +20,6 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -187,6 +185,7 @@ class _AppointmentCalendarState extends State { ), ); } else { + bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!); bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime); Navigator.of(context).pop(); Navigator.of(context).push( diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 7257dbf..ffe26ff 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -110,17 +110,19 @@ class DoctorCard extends StatelessWidget { AppCustomChipWidget( labelText: "Branch: ${isLoading ? "Olaya Hospital" : doctorsListResponseModel.projectName}".needTranslation, ).toShimmer2(isShow: isLoading), - AppCustomChipWidget( + doctorsListResponseModel.decimalDoctorRate != null ? AppCustomChipWidget( icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, labelText: "Rating: ${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".needTranslation, - ).toShimmer2(isShow: isLoading), - doctorsListResponseModel.nearestFreeSlot != null + ).toShimmer2(isShow: isLoading) : SizedBox(), + bookAppointmentsViewModel.isNearestAppointmentSelected + ? doctorsListResponseModel.nearestFreeSlot != null ? AppCustomChipWidget( labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)).needTranslation, backgroundColor: AppColors.successColor, textColor: AppColors.whiteColor, ).toShimmer2(isShow: isLoading) + : SizedBox.shrink() : SizedBox.shrink(), ], ), diff --git a/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart b/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart index 3be209b..18b656c 100644 --- a/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart +++ b/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart @@ -170,8 +170,6 @@ class _CmcSelectionReviewPageState extends State { latitude: double.parse(lat), longitude: double.parse(lng), address: hospitalName, - title: "Hospital Location".needTranslation, - showTitle: false, showAddress: false, padding: EdgeInsets.zero, onDirectionsTap: () => _launchDirections(selectedHospital), diff --git a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart index 5529b93..8b9ad89 100644 --- a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart +++ b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart @@ -366,7 +366,12 @@ class _ComprehensiveCheckupPageState extends State { if (pendingOrder == null && _selectedServiceId != null) { return SafeArea( top: false, - child: Padding( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h), child: CustomButton( borderWidth: 0, diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index db7c218..078b3a1 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -8,13 +8,10 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/models/feedback_type.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/widgets/feedback_appointment_selection.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -349,7 +346,7 @@ class FeedbackPage extends StatelessWidget { fontWeight: FontWeight.w500, borderRadius: 10.r, padding: EdgeInsets.symmetric(horizontal: 10.w), - height: isTablet || isFoldable ? 46.h : 40.h, + height: 40.h, icon: AppAssets.file_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, diff --git a/lib/presentation/contact_us/find_us_page.dart b/lib/presentation/contact_us/find_us_page.dart index a4e4258..5957bb8 100644 --- a/lib/presentation/contact_us/find_us_page.dart +++ b/lib/presentation/contact_us/find_us_page.dart @@ -4,14 +4,11 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; -import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/widgets/find_us_item_card.dart'; -import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_sick_leave_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/contact_us/widgets/find_us_item_card.dart b/lib/presentation/contact_us/widgets/find_us_item_card.dart index a59959f..96375c0 100644 --- a/lib/presentation/contact_us/widgets/find_us_item_card.dart +++ b/lib/presentation/contact_us/widgets/find_us_item_card.dart @@ -4,13 +4,11 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_hmg_locations.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:maps_launcher/maps_launcher.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/presentation/covid19test/covid19_landing_page.dart b/lib/presentation/covid19test/covid19_landing_page.dart new file mode 100644 index 0000000..62bd651 --- /dev/null +++ b/lib/presentation/covid19test/covid19_landing_page.dart @@ -0,0 +1,283 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +import 'covid_19_questionnaire.dart'; + +class Covid19LandingPage extends StatefulWidget { +const Covid19LandingPage({super.key}); + +@override +State createState() => _Covid19LandingPageState(); +} + +class _Covid19LandingPageState extends State { + + late HabibWalletViewModel habibWalletVM; + int? _selectedBranchIndex; + + @override + void initState() { + habibWalletVM = Provider.of(context, listen: false); + scheduleMicrotask(() { + getProjectList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "COVID-19", + child: Padding( + padding: EdgeInsets.all(24.w), + child: SingleChildScrollView( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Get the results in Few Hours" + .toText18( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + SizedBox(height: 16.h), + LocaleKeys.covid_info + .tr() + .toText14( + color: AppColors.greyTextColor, + weight: FontWeight.w400, + height: 1.6, + ), + ], + ), + ), + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + // borderRadius: 24.r, + hasShadow: true, + customBorder: BorderRadius.only(topLeft:Radius.circular(24.r) , topRight:Radius.circular(24.r)) + ), + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomButton( + text: "SelectLocation".needTranslation, + onPressed: () { + _showBranchBottomSheet(context); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 50.h, + iconSize: 18.h, + ).paddingSymmetrical(16.h, 24.w), + ], + ), + ), + ), + ], + )); + } + void _showBranchBottomSheet(BuildContext context, ) { + // Set first branch as selected by default + setState(() { + _selectedBranchIndex = 0; + }); + + showCommonBottomSheet( + + context, + title: "Select Branch".needTranslation, + height: ResponsiveExtension.screenHeight * 0.651, + child: StatefulBuilder( + builder: (context, setBottomSheetState) { + return Consumer( + builder: (context, habibWalletVM, child) { + + final hospitals = habibWalletVM.advancePaymentHospitals; + if (hospitals.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('No branches available'), + ), + ); + } + + return Column( + children: [ + Expanded( + child:Container( + margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h, bottom: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + hasShadow: true, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.r), + topRight: Radius.circular(24.r), + ), + ), child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + itemBuilder: (context, index) { + final branch = hospitals[index]; + final isSelected = _selectedBranchIndex == index; + return GestureDetector( + onTap: () { + setBottomSheetState(() { + _selectedBranchIndex = index; + }); + setState(() { + _selectedBranchIndex = index; + }); + }, + child: Container( + + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Row( + children: [ + // Radio button + Container( + width: 20.w, + height: 20.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + width: 2, + ), + ), + child: isSelected + ? Center( + child: Container( + width: 10.w, + height: 10.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primaryRedColor, + ), + ), + ) + : null, + ), + SizedBox(width: 12.w), + // Branch details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + SizedBox(height: 4.h), + (branch.desciption ?? 'Unknown').toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ], + ), + ), + // Location icon + + ], + ), + ), + ); + }, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemCount: hospitals.length, + )), + ), + // Next button + Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + hasShadow: true, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.r), + topRight: Radius.circular(24.r), + ), + ), + child: SafeArea( + top: false, + child: CustomButton( + text: "Next".needTranslation, + onPressed: (){ + + Navigator.of(context) + .push( + CustomPageRoute( + page: Covid19Questionnaire(selectedHospital: hospitals[_selectedBranchIndex!],), + ), + ); + + }, + backgroundColor: _selectedBranchIndex != null + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + borderColor: _selectedBranchIndex != null + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 56.h, + ), + ), + ), + ], + ); + }, + ); + }, + ), + + ); + } + + getProjectList() async{ + await habibWalletVM.getProjectsList(); + } +} + + diff --git a/lib/presentation/covid19test/covid_19_questionnaire.dart b/lib/presentation/covid19test/covid_19_questionnaire.dart new file mode 100644 index 0000000..8608d80 --- /dev/null +++ b/lib/presentation/covid19test/covid_19_questionnaire.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/covid_questionnare_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/presentation/covid19test/covid_review_screen.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/CustomSwitch.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + +import 'package:provider/provider.dart'; + +class Covid19Questionnaire extends StatefulWidget { + final HospitalsModel selectedHospital; + const Covid19Questionnaire({super.key, required this.selectedHospital}); + + @override + State createState() => _Covid19QuestionnaireState(); +} + +class _Covid19QuestionnaireState extends State { + late HmgServicesViewModel hmgServicesViewModel; + List qaList = []; + + + @override + void initState() { + hmgServicesViewModel = Provider.of(context, listen: false); + scheduleMicrotask(() { + setState(() { + qaList = hmgServicesViewModel.getQuestionsFromJson(); + }); + }); + super.initState(); + } + + void _toggleAnswer(int index, bool value) { + setState(() { + qaList[index].ans = value ? 1 : 0; + }); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "COVID-19", + bottomChild: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + hasShadow: true, + customBorder: BorderRadius.only( + topLeft: Radius.circular(24.r), + topRight: Radius.circular(24.r), + ), + ),child: CustomButton( + text: "Next".needTranslation, + onPressed: () { + moveToNextPage(context); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 56.h, + )), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.w), + child: Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Please answer below questionnaire:".toText14( + color: AppColors.textColor, + weight: FontWeight.w500, + ), + SizedBox(height: 20.h), + // Question list + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: qaList.length, + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemBuilder: (context, index) { + final question = qaList[index]; + final isAnswerYes = question.ans == 1; + + return Row( + children: [ + Expanded( + child: (question.questionEn ?? '').toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + ), + SizedBox(width: 12.w), + CustomSwitch( + value: isAnswerYes, + onChanged: (value) => _toggleAnswer(index, value), + ), + ], + ); + }, + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + // Next button + + ], + ), + ), + + ), + ); + + + } + moveToNextPage(BuildContext context) async{ + LoaderBottomSheet.showLoader(); + await hmgServicesViewModel.getCovidProcedureList(); + await hmgServicesViewModel.getPaymentInfo(procedureID: hmgServicesViewModel.covidTestProcedureList[0].procedureId!); + LoaderBottomSheet.hideLoader(); + Navigator.of(context) + .push( + CustomPageRoute( + page: CovidReviewScreen(selectedHospital: widget.selectedHospital, qaList: qaList), + ), + ); + } +} diff --git a/lib/presentation/covid19test/covid_payment_screen.dart b/lib/presentation/covid19test/covid_payment_screen.dart new file mode 100644 index 0000000..42e7736 --- /dev/null +++ b/lib/presentation/covid19test/covid_payment_screen.dart @@ -0,0 +1,565 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; + +// Added imports required by this file +import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; + +/// A reusable payment screen for COVID-related payments. +/// +/// This screen re-uses the same UI pattern and payment flow used by +/// `AppointmentPaymentPage` (in-app browser, Apple Pay and payfort status checks), +/// but it keeps the post-payment handling generic (shows success / failure) +/// so it can be safely used for COVID test purchases without appointment-specific APIs. +class CovidPaymentScreen extends StatefulWidget { + final double amount; + final int projectID; + final int clinicID; + final String procedureId; + final double taxAmount; + final String title; + + const CovidPaymentScreen({ + super.key, + required this.amount, + required this.projectID, + required this.clinicID, + required this.procedureId, + this.taxAmount = 0.0, + this.title = "COVID Payment", + }); + + @override + State createState() => _CovidPaymentScreenState(); +} + +class _CovidPaymentScreenState extends State { + late PayfortViewModel payfortViewModel; + late AppState appState; + + MyInAppBrowser? browser; + String selectedPaymentMethod = ""; + String transID = ""; + + bool isShowTamara = false; // placeholder: could be enabled based on remote config + + @override + void initState() { + super.initState(); + // initialize payfort view model when the widget is ready + scheduleMicrotask(() { + payfortViewModel = Provider.of(context, listen: false); + payfortViewModel.initPayfortViewModel(); + payfortViewModel.setIsApplePayConfigurationLoading(false); + // Optionally compute if Tamara should be shown by calling a remote config API. + // For now keep it false (unless the app provides an API for it). + // Enable Tamara payment option for COVID screen + setState(() { + isShowTamara = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + payfortViewModel = Provider.of(context); + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: widget.title.needTranslation, + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + "Total amount to pay".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(( (widget.amount - widget.taxAmount).toString()).toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + // Show VAT amount passed from review screen + Utils.getPaymentAmountWithSymbol((widget.taxAmount.toString()).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(widget.amount.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + + // Apple Pay (iOS) + Platform.isIOS + ? Utils.buildSvgWithAssets( + icon: AppAssets.apple_pay_button, + width: 200.h, + height: 80.h, + fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h).onPress(() { + if (Utils.havePrivilege(103)) { + startApplePay(); + } else { + openPaymentURL("ApplePay"); + } + }) + : SizedBox(height: 12.h), + SizedBox(height: 12.h), + + // Action buttons: Cancel + Next (Next opens default payment flow - e.g. Visa) + // Padding( + // padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 24.h), + // child: Row( + // children: [ + // Expanded( + // child: CustomButton( + // height: 56.h, + // text: LocaleKeys.cancel.tr(), + // onPressed: () { + // Navigator.of(context).pop(); + // }, + // backgroundColor: AppColors.secondaryLightRedColor, + // borderColor: AppColors.secondaryLightRedColor, + // textColor: AppColors.primaryRedColor, + // icon: AppAssets.cancel, + // iconColor: AppColors.primaryRedColor, + // borderRadius: 12.r, + // ), + // ), + // SizedBox(width: 8.h), + // Expanded( + // child: CustomButton( + // height: 56.h, + // text: "Next".needTranslation, + // onPressed: () { + // // Default to Visa for Next + // selectedPaymentMethod = "VISA"; + // openPaymentURL("visa"); + // }, + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // textColor: AppColors.whiteColor, + // fontSize: 16.f, + // fontWeight: FontWeight.w500, + // borderRadius: 12.r, + // padding: EdgeInsets.symmetric(horizontal: 10.w), + // icon: AppAssets.add_icon, + // iconColor: AppColors.whiteColor, + // iconSize: 18.h, + // ), + // ), + // ], + // ), + // ), + ], + ), + ).paddingSymmetrical(0.h, 0.h), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + + // MADA tile + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(AppAssets.mada, width: 72.h, height: 25.h), + SizedBox(height: 16.h), + "Mada".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "MADA"; + openPaymentURL("MADA"); + }), + + SizedBox(height: 16.h), + + // Visa / Mastercard tile + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset(AppAssets.visa, width: 50.h, height: 50.h), + SizedBox(width: 8.h), + Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h), + ], + ), + SizedBox(height: 16.h), + "Visa or Mastercard".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "VISA"; + openPaymentURL("VISA"); + }), + + SizedBox(height: 16.h), + + // Optional Tamara tile (shown only if enabled) + isShowTamara + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + AppAssets.tamaraEng, + width: 72.h, + height: 25.h, + fit: BoxFit.contain, + // If PNG fails to load for any reason, log and fallback to SVG asset + errorBuilder: (context, error, stackTrace) { + debugPrint('Failed to load Tamara PNG asset: $error'); + return Utils.buildSvgWithAssets( + icon: AppAssets.tamara, + width: 72.h, + height: 25.h, + fit: BoxFit.contain, + ); + }, + ), + SizedBox(height: 16.h), + "Tamara".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(width: 8.h), + const Spacer(), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + selectedPaymentMethod = "TAMARA"; + openPaymentURL("TAMARA"); + }) + : SizedBox.shrink(), + + SizedBox(height: 24.h), + + // Bottom payment summary + + SizedBox(height: 24.h), + ], + ), + ), + + ), + ); + } + + void onBrowserLoadStart(String url) { + // Generic loader hook: detect success / error patterns from the in-app browser. + // Keep parsing defensive: use Uri.parse where possible. + try { + final uri = Uri.tryParse(url); + if (selectedPaymentMethod == "TAMARA" && uri != null) { + // tamara returns different query param names depending on platform; defensive checks + final params = uri.queryParameters; + if (params.isNotEmpty) { + // example keys: 'status', 'AuthorizePaymentId' (android) or 'paymentStatus', 'orderId' (iOS) + } + } + } catch (e) { + debugPrint('onBrowserLoadStart parse error: $e'); + } + + MyInAppBrowser.successURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = true; + return; + } + }); + + MyInAppBrowser.errorURLS.forEach((element) { + if (url.contains(element)) { + browser?.close(); + MyInAppBrowser.isPaymentDone = false; + return; + } + }); + } + + void onBrowserExit(bool isPaymentMade) async { + // When browser closes, check payment status using payfort view model + await checkPaymentStatus(); + } + + Future checkPaymentStatus() async { + LoaderBottomSheet.showLoader(loadingText: "Checking payment status, Please wait...".needTranslation); + try { + await payfortViewModel.checkPaymentStatus(transactionID: transID, onSuccess: (apiResponse) async { + // treat any successful responseMessage as success; otherwise show generic error + final success = payfortViewModel.payfortCheckPaymentStatusResponseModel?.responseMessage?.toLowerCase() == 'success'; + LoaderBottomSheet.hideLoader(); + if (success) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Payment successful".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment Failed! Please try again.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + } catch (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } + + void openPaymentURL(String paymentMethod) { + browser = MyInAppBrowser(onExitCallback: onBrowserExit, onLoadStartCallback: onBrowserLoadStart, context: context); + transID = Utils.getAppointmentTransID(widget.projectID, widget.clinicID, DateTime.now().millisecondsSinceEpoch); + + // Open payment browser with essential parameters; many fields are simplified here + browser?.openPaymentBrowser( + widget.amount, + "COVID Test Payment", + transID, + widget.projectID.toString(), + "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com", + selectedPaymentMethod, + appState.getAuthenticatedUser()?.patientType.toString() ?? "", + appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "", + appState.getAuthenticatedUser()?.patientId.toString() ?? "", + appState.getAuthenticatedUser() ?? (null as dynamic), + browser!, + false, + "2", + "", + context, + DateTime.now().toString(), + "", + 0, + 0, + "3", + ); + } + + void startApplePay() async { + showCommonBottomSheet( + context, + child: Utils.getLoadingWidget(), + callBackFunc: (str) {}, + title: "", + height: ResponsiveExtension.screenHeight * 0.3, + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + ); + + transID = Utils.getAppointmentTransID(widget.projectID, widget.clinicID, DateTime.now().millisecondsSinceEpoch); + + // Prepare a minimal apple pay request using payfortViewModel's configuration + try { + await payfortViewModel.getPayfortConfigurations(serviceId: 0, projectId: widget.projectID, integrationId: 2); + + // Build minimal apple pay request (model omitted here to keep things generic) + ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest( + clientRequestID: transID, + clinicID: widget.clinicID, + currency: appState.getAuthenticatedUser() != null && appState.getAuthenticatedUser()!.outSa == 0 ? "SAR" : "AED", + customerEmail: "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com", + customerID: appState.getAuthenticatedUser()?.patientId, + customerName: appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "", + deviceToken: await Utils.getStringFromPrefs(CacheConst.pushToken), + voipToken: await Utils.getStringFromPrefs(CacheConst.voipToken), + doctorID: 0, + projectID: widget.projectID.toString(), + serviceID: "0", + channelID: 3, + patientID: appState.getAuthenticatedUser()?.patientId, + patientTypeID: appState.getAuthenticatedUser()?.patientType, + patientOutSA: appState.getAuthenticatedUser()?.outSa, + appointmentDate: DateTime.now().toString(), + appointmentNo: 0, + orderDescription: "COVID Test Payment", + liveServiceID: "0", + latitude: "0.0", + longitude: "0.0", + amount: widget.amount.toString(), + isSchedule: "0", + language: appState.isArabic() ? 'ar' : 'en', + languageID: appState.isArabic() ? 1 : 2, + userName: appState.getAuthenticatedUser()?.patientId, + responseContinueURL: "http://hmg.com/Documents/success.html", + backClickUrl: "http://hmg.com/Documents/success.html", + paymentOption: "ApplePay", + isMobSDK: true, + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel?.merchantIdentifier, + commandType: "PURCHASE", + signature: payfortViewModel.payfortProjectDetailsRespModel?.signature, + accessCode: payfortViewModel.payfortProjectDetailsRespModel?.accessCode, + shaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaRequest, + shaResponsePhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaResponse, + returnURL: "", + ); + + await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) { + // Start apple pay flow + payfortViewModel.paymentWithApplePay( + customerName: appState.getAuthenticatedUser() != null ? "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" : "", + customerEmail: "CustID_${appState.getAuthenticatedUser()?.patientId ?? ''}@HMG.com", + orderDescription: "COVID Test Payment", + orderAmount: widget.amount, + merchantReference: transID, + merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel?.merchantIdentifier ?? "", + applePayAccessCode: payfortViewModel.payfortProjectDetailsRespModel?.accessCode ?? "", + applePayShaRequestPhrase: payfortViewModel.payfortProjectDetailsRespModel?.shaRequest ?? "", + currency: appState.getAuthenticatedUser() != null && appState.getAuthenticatedUser()!.outSa == 0 ? "SAR" : "AED", + onFailed: (failureResult) async { + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: failureResult.message.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSucceeded: (successResult) async { + Navigator.of(context).pop(); + selectedPaymentMethod = successResult.paymentOption ?? "VISA"; + await checkPaymentStatus(); + }, + ); + }).catchError((e) { + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: e.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } catch (e) { + Navigator.of(context).pop(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: e.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } + } + diff --git a/lib/presentation/covid19test/covid_review_screen.dart b/lib/presentation/covid19test/covid_review_screen.dart new file mode 100644 index 0000000..7ccbb02 --- /dev/null +++ b/lib/presentation/covid19test/covid_review_screen.dart @@ -0,0 +1,441 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/covid_get_test_proceedure_resp.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/covid_questionnare_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/presentation/covid19test/covid_payment_screen.dart'; + +class CovidReviewScreen extends StatefulWidget { + + final HospitalsModel selectedHospital; + final List qaList; + + const CovidReviewScreen({super.key, required this.selectedHospital, required this.qaList}); + + @override + State createState() => _CovidReviewScreenState(); +} + +class _CovidReviewScreenState extends State { + + late HmgServicesViewModel hmgServicesViewModel; + bool _acceptedTerms =false; + Covid19GetTestProceduresResp? _selectedProcedure; + @override + void initState() { + super.initState(); + hmgServicesViewModel = Provider.of(context, listen: false); + scheduleMicrotask(() { + + }); + + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "COVID-19", + bottomChild: Consumer(builder: (context, vm, _) { + final info = vm.covidPaymentInfo; + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (info == null) ...[ + // show a placeholder/loading while payment info is null + SizedBox(height: 24.h), + Center(child: CircularProgressIndicator()), + SizedBox(height: 24.h), + ] else ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText18(isBold: true), + Utils.getPaymentAmountWithSymbol( + (info.patientShareField ?? 0).toString().toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true), + ], + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Tax Amount".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + (info.patientTaxAmountField ?? 0).toString().toText16(isBold: true), + AppColors.blackColor, + 13, + isSaudiCurrency: true) + ], + ), + SizedBox(height: 18.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 150.h, + child: Utils.getPaymentMethods(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Utils.getPaymentAmountWithSymbol( + (info.patientShareWithTaxField ?? 0).toString().toText24(isBold: true), + AppColors.blackColor, + 17, + isSaudiCurrency: true), + ], + ), + ], + ) + ], + ).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h), + + GestureDetector( + onTap: () { + setState(() { + _acceptedTerms = !_acceptedTerms; + }); + }, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + child: Row( + children: [ + Container( + width: 20.w, + height: 20.h, + decoration: BoxDecoration( + color: _acceptedTerms + ? AppColors.primaryRedColor + : AppColors.whiteColor, + border: Border.all( + color: _acceptedTerms + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + width: 2, + ), + borderRadius: BorderRadius.circular(4.r), + ), + child: _acceptedTerms + ? Icon( + Icons.check, + size: 14.h, + color: AppColors.whiteColor, + ) + : null, + ), + SizedBox(width: 12.w), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + fontSize: 14.f, + color: AppColors.textColor, + fontWeight: FontWeight.w400, + ), + children: [ + const TextSpan(text: "I agree to the "), + TextSpan( + text: "terms and conditions", + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + decorationColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Two-button layout: Cancel (left) and Next (right) + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 24.h), + child: Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.cancel.tr(), + onPressed: () { + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + // icon: AppAssets.cancel, + // iconColor: AppColors.primaryRedColor, + borderRadius: 12.r, + ), + ), + SizedBox(width: 8.h), + Expanded( + child: CustomButton( + height: 56.h, + text: "Next".needTranslation, + onPressed: () async { + // Validate selection and payment info + if (_selectedProcedure == null) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Please select a procedure".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + return; + } + + if (info == null) { + // If payment info missing, attempt to fetch + LoaderBottomSheet.showLoader(); + try { + await hmgServicesViewModel.getPaymentInfo(procedureID: _selectedProcedure!.procedureId!); + } catch (e) { + debugPrint('getPaymentInfo error: $e'); + } finally { + LoaderBottomSheet.hideLoader(); + } + + if (hmgServicesViewModel.covidPaymentInfo == null) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Payment information not available".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + return; + } + } + + // Compute amount and project/clinic IDs defensively + final paymentInfo = hmgServicesViewModel.covidPaymentInfo ?? info!; + final double amount = paymentInfo.patientShareWithTaxField ?? (paymentInfo.patientShareField?.toDouble() ?? 0.0); + + // projectID may be int or string; parse defensively + int projectID = 0; + try { + final p = widget.selectedHospital.mainProjectID; + if (p is int) projectID = p; + else if (p is String) projectID = int.tryParse(p) ?? 0; + } catch (_) {} + + int clinicID = 0; + try { + clinicID = int.tryParse(widget.selectedHospital.setupID ?? '') ?? int.tryParse(widget.selectedHospital.iD?.toString() ?? '') ?? 0; + } catch (_) {} + + // Navigate to payment screen + Navigator.of(context).push( + CustomPageRoute( + page: CovidPaymentScreen( + amount: amount, + projectID: projectID, + clinicID: clinicID, + procedureId: _selectedProcedure!.procedureId ?? '', + taxAmount: paymentInfo.patientTaxAmountField?.toDouble() ?? 0.0, + ), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + // icon: AppAssets.add_icon, + // iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + ), + ], + ), + ) + ] + ], + ), + ), + ); + }), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + "Please select the procedure:".toText14( + color: AppColors.textColor, + weight: FontWeight.w500, + ), + SizedBox(height: 16.h), + + Consumer( + builder: (context, vm, _) { + final procedures = vm.covidTestProcedureList; + + // ensure default selection is first item (once available) + if (_selectedProcedure == null && procedures.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _selectedProcedure = procedures[0]; + }); + + // also fetch payment info for default selection and show loader while loading + if (procedures[0].procedureId != null) { + LoaderBottomSheet.showLoader(); + hmgServicesViewModel + .getPaymentInfo(procedureID: procedures[0].procedureId!) + .whenComplete(() { + LoaderBottomSheet.hideLoader(); + }); + } + } + }); + } + + // Use a shrink-wrapped ListView.separated and match prescription styling + return Container( + + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.h), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: procedures.length, + separatorBuilder: (context, index) => Divider( + height: 1.h, + thickness: 1.h, + color: AppColors.borderOnlyColor.withValues(alpha: 0.05), + ).paddingOnly(top:8,bottom:16.h), + itemBuilder: (context, index) { + final item = procedures[index]; + // Let the radio option widget manage its own padding to avoid doubled spacing + return _buildRadioOption(value: item, title: item.procedureName ?? ''); + }, + ).paddingOnly(bottom:16.h) + ), + ); + }, + ), + ], + ), + ), + ), + ); + + } + + Widget _buildRadioOption({ + required Covid19GetTestProceduresResp value, + required String title, + }) { + final bool isSelected = _selectedProcedure?.procedureId == value.procedureId; + + return GestureDetector( + onTap: () async { + setState(() { + _selectedProcedure = value; + }); + + // show bottomsheet loader while fetching payment info + if (value.procedureId != null) { + LoaderBottomSheet.showLoader(); + try { + await hmgServicesViewModel.getPaymentInfo(procedureID: value.procedureId!, projectID: widget.selectedHospital.mainProjectID); + } catch (e) { + debugPrint('getPaymentInfo error: $e'); + } finally { + LoaderBottomSheet.hideLoader(); + } + } + }, + + + child: Row( + children: [ + Container( + width: 20.h, + height: 20.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? AppColors.primaryRedColor + : AppColors.greyTextColor.withValues(alpha: 0.3), + width: 2, + ), + ), + child: isSelected + ? Center( + child: Container( + width: 10.h, + height: 10.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primaryRedColor, + ), + ), + ) + : null, + ), + SizedBox(width: 12.h), + Expanded( + child: title.toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + ), + ], + ) + // Keep only bottom padding here and rely on the surrounding container's left/right inset + .paddingOnly(left: 0.h, right: 0.h, top: 0.h, bottom: 12.h), + ); + + } + } diff --git a/lib/presentation/e_referral/e_referral_form_manager.dart b/lib/presentation/e_referral/e_referral_form_manager.dart index f7093c5..9e5cbe8 100644 --- a/lib/presentation/e_referral/e_referral_form_manager.dart +++ b/lib/presentation/e_referral/e_referral_form_manager.dart @@ -1,6 +1,5 @@ // managers/referral_form_manager.dart import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/common_models/nationality_country_model.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/create_e_referral_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_all_cities_resp_model.dart'; diff --git a/lib/presentation/e_referral/e_referral_search_result.dart b/lib/presentation/e_referral/e_referral_search_result.dart index ba31f1b..8702d5a 100644 --- a/lib/presentation/e_referral/e_referral_search_result.dart +++ b/lib/presentation/e_referral/e_referral_search_result.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; diff --git a/lib/presentation/e_referral/new_e_referral.dart b/lib/presentation/e_referral/new_e_referral.dart index b28b8df..3083de1 100644 --- a/lib/presentation/e_referral/new_e_referral.dart +++ b/lib/presentation/e_referral/new_e_referral.dart @@ -24,6 +24,7 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; import 'package:provider/provider.dart'; + import 'e-referral_validator.dart'; import 'e_referral_form_manager.dart'; @@ -43,7 +44,6 @@ class _NewReferralPageState extends State { double widthOfOneState = ((ResponsiveExtension.screenWidth) / 3) - (20.h); - @override void initState() { super.initState(); @@ -116,7 +116,7 @@ class _NewReferralPageState extends State { cityCode: _formManager.formData.patientCity!.iD!.toString(), cityName: _formManager.formData.patientCity!.description, requesterName: _formManager.formData.requesterName, - requesterContactNo: _formManager.formData.countryEnum.countryCode + _formManager.formData.requesterPhone, + requesterContactNo: _formManager.formData.countryEnum.countryCode + _formManager.formData.requesterPhone, requesterRelationship: _formManager.formData.relationship?.iD, otherRelationship: _formManager.formData.relationship!.iD.toString(), fullName: _formManager.formData.patientName, @@ -133,10 +133,8 @@ class _NewReferralPageState extends State { hmgServicesVM.createEReferral( requestModel: createReferralRequestModel, onSuccess: (GenericApiModel response) { - showSuccessBottomSheet(int.parse(response.data), hmgServicesVM); LoaderBottomSheet.hideLoader(); - }, onError: (errorMessage) { // Handle error (e.g., show error message) @@ -146,9 +144,6 @@ class _NewReferralPageState extends State { } void _loadData() { - - - final authVM = context.read(); final habibWalletVM = context.read(); final hmgServicesVM = context.read(); @@ -179,7 +174,7 @@ class _NewReferralPageState extends State { color: Colors.white, padding: EdgeInsets.all(ResponsiveExtension(20).h), child: CustomButton( - text: _currentStep <=1 ? LocaleKeys.next.tr() : LocaleKeys.submit.tr(), + text: _currentStep <= 1 ? LocaleKeys.next.tr() : LocaleKeys.submit.tr(), // icon: AppAssets.search_icon, iconColor: Colors.white, onPressed: () => {_handleNextStep()}, @@ -188,7 +183,7 @@ class _NewReferralPageState extends State { child: ChangeNotifierProvider.value( value: _formManager, child: SizedBox( - height: ResponsiveExtension.screenHeight * 0.65, + height: ResponsiveExtension.screenHeight * 0.65, child: Column( children: [ const SizedBox(height: 8), @@ -220,7 +215,6 @@ class _NewReferralPageState extends State { // ); } - showSuccessBottomSheet(int requestId, HmgServicesViewModel hmgServicesViewModel) { return showCommonBottomSheetWithoutHeight( context, @@ -232,9 +226,9 @@ class _NewReferralPageState extends State { Row( children: [ "Here is your Referral #: ".needTranslation.toText14( - color: AppColors.textColorLight, - weight: FontWeight.w500, - ), + color: AppColors.textColorLight, + weight: FontWeight.w500, + ), SizedBox(width: 4.w), ("$requestId").toText16(isBold: true), ], @@ -249,7 +243,7 @@ class _NewReferralPageState extends State { onPressed: () { context.pop(); context.pop(); - _currentStep =0; + _currentStep = 0; }, textColor: AppColors.whiteColor, ), diff --git a/lib/presentation/e_referral/widget/e_referral_other_details.dart b/lib/presentation/e_referral/widget/e_referral_other_details.dart index fd1649f..6efd063 100644 --- a/lib/presentation/e_referral/widget/e_referral_other_details.dart +++ b/lib/presentation/e_referral/widget/e_referral_other_details.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; @@ -233,7 +232,6 @@ class _OtherDetailsStepState extends State { } void _showBranchBottomSheet(BuildContext context, ReferralFormManager formManager) { - final habibWalletVM = context.read(); showCommonBottomSheetWithoutHeight( context, diff --git a/lib/presentation/emergency_services/RRT/rrt_map_screen.dart b/lib/presentation/emergency_services/RRT/rrt_map_screen.dart index 3a17e5d..afe68b6 100644 --- a/lib/presentation/emergency_services/RRT/rrt_map_screen.dart +++ b/lib/presentation/emergency_services/RRT/rrt_map_screen.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -8,13 +7,11 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart'; import 'package:hmg_patient_app_new/features/location/GeocodeResponse.dart'; import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart'; import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/AddressItem.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart' show AppointmentBottomSheet; diff --git a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart index 66894b6..13e0106 100644 --- a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart +++ b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart @@ -1,5 +1,4 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; @@ -11,8 +10,6 @@ import 'package:hmg_patient_app_new/features/emergency_services/models/resp_mode import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/radio/custom_radio_button.dart'; import 'package:provider/provider.dart'; class RrtRequestTypeSelect extends StatelessWidget { diff --git a/lib/presentation/emergency_services/RRT/terms_and_condition.dart b/lib/presentation/emergency_services/RRT/terms_and_condition.dart index 8a50792..1d1dac1 100644 --- a/lib/presentation/emergency_services/RRT/terms_and_condition.dart +++ b/lib/presentation/emergency_services/RRT/terms_and_condition.dart @@ -1,14 +1,10 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:provider/provider.dart'; class TermsAndCondition extends StatelessWidget { final String termsAndCondition; diff --git a/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart b/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart index 83c16b6..396e509 100644 --- a/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart +++ b/lib/presentation/emergency_services/call_ambulance/requesting_services_page.dart @@ -1,15 +1,10 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/PatientER_RC.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/tracking_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart' - show CustomPageRoute; import 'package:lottie/lottie.dart'; class RequestingServicesPage extends StatelessWidget { diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart b/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart index 71e589e..7d24393 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart @@ -5,22 +5,13 @@ import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/debouncer.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:provider/provider.dart'; class HospitalBottomSheetBody extends StatelessWidget { diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart b/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart index de20d1a..be1d380 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart @@ -2,12 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart' show AppAssets; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/PatientERTransportationMethod.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart' show TransportOptionItem; -import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:provider/provider.dart'; class AmbulanceOptionSelectionBottomSheet extends StatelessWidget { diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart b/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart index 6f5db92..e1f7420 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/appointment_bottom_sheet.dart @@ -1,13 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart'; -import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_card.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:provider/provider.dart'; class AppointmentBottomSheet extends StatelessWidget{ diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart b/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart index 3b8d8d7..1b85165 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart @@ -2,12 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:provider/provider.dart' show Consumer; class TypeSelectionWidget extends StatelessWidget { final String hmcCount; diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart index 0dfd081..ee061b7 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart @@ -8,7 +8,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart index 3aa975d..f48efe7 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart @@ -10,7 +10,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart index 0b22417..d44686c 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -10,17 +9,10 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; -import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/nfc/nfc_reader_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; class ErOnlineCheckinSelectCheckinBottomSheet extends StatelessWidget { ErOnlineCheckinSelectCheckinBottomSheet({super.key, required this.projectID}); diff --git a/lib/presentation/emergency_services/history/er_history_listing.dart b/lib/presentation/emergency_services/history/er_history_listing.dart index b98f2f1..fe1ed31 100644 --- a/lib/presentation/emergency_services/history/er_history_listing.dart +++ b/lib/presentation/emergency_services/history/er_history_listing.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart' show AppAssets; import 'package:hmg_patient_app_new/core/app_export.dart'; @@ -9,7 +8,6 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/ambulance_history_item.dart' show AmbulanceHistoryItem; import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/rrt_item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/emergency_services/history/widget/rrt_item.dart b/lib/presentation/emergency_services/history/widget/rrt_item.dart index 61ecced..dfb6e79 100644 --- a/lib/presentation/emergency_services/history/widget/rrt_item.dart +++ b/lib/presentation/emergency_services/history/widget/rrt_item.dart @@ -5,7 +5,6 @@ import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/history/widget/RequestStatus.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/emergency_services/nearest_er_page.dart b/lib/presentation/emergency_services/nearest_er_page.dart index 13373f1..16863bf 100644 --- a/lib/presentation/emergency_services/nearest_er_page.dart +++ b/lib/presentation/emergency_services/nearest_er_page.dart @@ -14,7 +14,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart' show Selector, WatchContext, ReadContext; -import '../../core/enums.dart' show SelectionTypeEnum; import '../../core/utils/debouncer.dart' show Debouncer; class NearestErPage extends StatefulWidget { diff --git a/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart b/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart index 559a0a5..c5301ea 100644 --- a/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart +++ b/lib/presentation/emergency_services/widgets/location_input_bottom_sheet.dart @@ -18,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; import '../../../theme/colors.dart'; -import '../../appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; class LocationInputBottomSheet extends StatelessWidget { final Debouncer debouncer = Debouncer(milliseconds: 500); diff --git a/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart b/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart index b7a16c5..086e0da 100644 --- a/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart +++ b/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart @@ -1,21 +1,10 @@ -import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/hospital_list_item.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; class SelectHospitalBottomSheet extends StatelessWidget { diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart new file mode 100644 index 0000000..42cba5d --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bf.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/blood_sugar.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bmi.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/bmr.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/calories.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/crabs.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/dduedate.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ibw.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/ovulation.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/triglycerides.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class HealthCalculatorDetailedPage extends StatefulWidget { + HealthCalculatorsTypeEnum calculatorType; + int? clinicID; + int? calculationID; + + HealthCalculatorDetailedPage({super.key, required this.calculatorType, this.clinicID, this.calculationID}); + + @override + State createState() => _HealthCalculatorDetailedPageState(); +} + +class _HealthCalculatorDetailedPageState extends State { + dynamic calculatedResult; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => HealthCalcualtorViewModel(), + child: Consumer(builder: (context, provider, _) { + return CollapsingListView( + title: widget.calculatorType.displayName, + bottomChild: widget.calculatorType == HealthCalculatorsTypeEnum.bloodSugar || + widget.calculatorType == HealthCalculatorsTypeEnum.bloodCholesterol || + widget.calculatorType == HealthCalculatorsTypeEnum.triglycerides + ? SizedBox() + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.r), topRight: Radius.circular(24.r))), + padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.h), + child: CustomButton( + text: widget.calculatorType == HealthCalculatorsTypeEnum.bloodSugar || + widget.calculatorType == HealthCalculatorsTypeEnum.bloodCholesterol || + widget.calculatorType == HealthCalculatorsTypeEnum.triglycerides + ? "Convert".needTranslation + : "Calculate".needTranslation, + onPressed: () { + if (calculatedResult == null) return; + + DialogService dialogService = getIt.get(); + Navigator.of(context).push(CustomPageRoute( + page: SelectClinicPage(calculatorType: widget.calculatorType, calculatedResult: calculatedResult), + )); + }, + icon: null, + fontSize: 16.f, + backgroundColor: calculatedResult == null ? AppColors.bgRedLightColor : AppColors.primaryRedColor, + borderColor: calculatedResult == null ? AppColors.bgRedLightColor : AppColors.primaryRedColor, + borderRadius: 12.r, + fontWeight: FontWeight.w500)), + child: getCalculatorWidget( + type: widget.calculatorType, + onCalculate: (result) { + // result may be directly provided by widget, but prefer provider maps if available + + switch (widget.calculatorType) { + case HealthCalculatorsTypeEnum.bmi: + calculatedResult = provider.bmiResultMap != null + ? {...?provider.bmiResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.calories: + case HealthCalculatorsTypeEnum.bmr: + calculatedResult = provider.caloriesResultMap != null + ? {...?provider.caloriesResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.idealBodyWeight: + calculatedResult = provider.ibwResultMap != null + ? {...?provider.ibwResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.bodyFat: + calculatedResult = provider.bodyFatResultMap != null + ? {...?provider.bodyFatResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.crabsProteinFat: + calculatedResult = provider.macrosResultMap != null + ? {...?provider.macrosResultMap, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.ovulation: + calculatedResult = (provider.ovulationResult is Map) + ? {...provider.ovulationResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.ovulationResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.deliveryDueDate: + calculatedResult = (provider.deliveryResult is Map) + ? {...provider.deliveryResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.deliveryResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.bloodSugar: + calculatedResult = (provider.bloodSugarResult is Map) + ? {...provider.bloodSugarResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.bloodSugarResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.bloodCholesterol: + calculatedResult = (provider.bloodCholesterolResult is Map) + ? {...provider.bloodCholesterolResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.bloodCholesterolResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + case HealthCalculatorsTypeEnum.triglycerides: + calculatedResult = (provider.triglyceridesResult is Map) + ? {...provider.triglyceridesResult as Map, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID} + : {'result': provider.triglyceridesResult ?? result, 'clinicId': widget.clinicID, 'calculationID': widget.calculationID}; + break; + } + // switch (widget.calculatorType) { + // case HealthCalculatorsTypeEnum.bmi: + // calculatedResult = provider.bmiResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.calories: + // case HealthCalculatorsTypeEnum.bmr: + // calculatedResult = provider.caloriesResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.idealBodyWeight: + // calculatedResult = provider.ibwResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.bodyFat: + // calculatedResult = provider.bodyFatResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.crabsProteinFat: + // calculatedResult = provider.macrosResultMap ?? result; + // break; + // case HealthCalculatorsTypeEnum.ovulation: + // calculatedResult = provider.ovulationResult ?? result; + // break; + // case HealthCalculatorsTypeEnum.deliveryDueDate: + // calculatedResult = provider.deliveryResult ?? result; + // break; + // case HealthCalculatorsTypeEnum.bloodSugar: + // calculatedResult = provider.bloodSugarResult ?? result; + // case HealthCalculatorsTypeEnum.bloodCholesterol: + // calculatedResult = provider.bloodCholesterolResult ?? result; + // case HealthCalculatorsTypeEnum.triglycerides: + // calculatedResult = provider.triglyceridesResult ?? result; + // } + }, + ).paddingSymmetrical(20.w, 24.h), + ); + }), + ); + } + + Widget getCalculatorWidget({required HealthCalculatorsTypeEnum type, required Function(dynamic result) onCalculate}) { + switch (widget.calculatorType) { + case HealthCalculatorsTypeEnum.bmi: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BMIWidget(onChange: onCalculate), + ); + + case HealthCalculatorsTypeEnum.calories: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: CaloriesWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bmr: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BMRWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.idealBodyWeight: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: IdealBodyWeightWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bodyFat: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BodyFatWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.crabsProteinFat: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: CrabsWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.ovulation: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: OvulationWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.deliveryDueDate: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: DeliveryDueDWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bloodSugar: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BloodSugarWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.bloodCholesterol: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: BloodCholesterolWidget(onChange: onCalculate), + ); + case HealthCalculatorsTypeEnum.triglycerides: + return Container( + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, customBorder: BorderRadius.all(Radius.circular(24.r))), + child: TriglyceridesWidget(onChange: onCalculate), + ); + } + } +} diff --git a/lib/presentation/health_calculators_and_converts/health_calculator_view_model.dart b/lib/presentation/health_calculators_and_converts/health_calculator_view_model.dart new file mode 100644 index 0000000..fb97eab --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/health_calculator_view_model.dart @@ -0,0 +1,1208 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; + +class HealthCalcualtorViewModel extends ChangeNotifier { + // BMI + double? bmiResult; + String? bmiCategory; + + // BMR / Calories + double? bmrResult; + double? caloriesResult; + String? bmrRange; + String? calorieRange; + + // IBW + double? ibwResult; + double? weightDifference; + String? weightStatus; + + // Body Fat + double? bodyFatResult; + String? bodyFatCategory; + + // Crabs/Protein/Fat + double? carbsCalories; + double? proteinCalories; + double? fatCalories; + double? carbsGrams; + double? proteinGrams; + double? fatGrams; + String? dietType; + + // Ovulation + Map? ovulationResult; + + // Delivery + Map? deliveryResult; + + Map? bloodSugarResult; + Map? bloodCholesterolResult; + Map? triglyceridesResult; + + String? _mgdlValue; + + String? get mgdlValue => _mgdlValue; + + String? _mmolValue; + + String? get mmolValue => _mmolValue; + + // Store the original entered value and which field it was entered in + String? _originalValue; + String _originalUnit = 'mg/dL'; // which field the original value was entered in + bool _isSwapped = false; // toggle state for switch + + String _activeUnit = 'mg/dL'; // current source unit + String get activeUnit => _activeUnit; + + // ================== BLOOD CHOLESTEROL ================== + String? _cholMgdlValue; + String? _cholMmolValue; + String? _cholOriginalValue; + String _cholOriginalUnit = 'mg/dL'; + bool _cholIsSwapped = false; + String _cholActiveUnit = 'mg/dL'; + + String? get cholMgdlValue => _cholMgdlValue; + + String? get cholMmolValue => _cholMmolValue; + + String get cholActiveUnit => _cholActiveUnit; + + // ================== TRIGLYCERIDES ================== + String? _triMgdlValue; + String? _triMmolValue; + String? _triOriginalValue; + String _triOriginalUnit = 'mg/dL'; + bool _triIsSwapped = false; + String _triActiveUnit = 'mg/dL'; + + String? get triMgdlValue => _triMgdlValue; + + String? get triMmolValue => _triMmolValue; + + String get triActiveUnit => _triActiveUnit; + + // Generic helpers + + void calculateBMI({required String heightText, required String weightText, required String heightUnit, required String weightUnit}) { + if (heightText.trim().isEmpty || weightText.trim().isEmpty) { + bmiResult = null; + bmiCategory = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double weight = double.parse(weightText.trim()); + + double heightInMeters = _convertHeightToMeters(height, heightUnit); + double weightInKg = _convertWeightToKg(weight, weightUnit); + + if (heightInMeters <= 0 || weightInKg <= 0) { + bmiResult = null; + bmiCategory = null; + notifyListeners(); + return; + } + + double bmi = weightInKg / (heightInMeters * heightInMeters); + String category = _getBMICategory(bmi); + + bmiResult = bmi; + bmiCategory = category; + notifyListeners(); + } catch (e) { + bmiResult = null; + bmiCategory = null; + notifyListeners(); + } + } + + double _convertHeightToMeters(double height, String unit) { + switch (unit) { + case 'cm': + return height / 100; + case 'm': + return height; + case 'ft': + return height * 0.3048; + case 'in': + return height * 0.0254; + default: + return height / 100; + } + } + + double _convertWeightToKg(double weight, String unit) { + switch (unit) { + case 'kg': + return weight; + case 'lb': + return weight * 0.453592; + default: + return weight; + } + } + + String _getBMICategory(double bmi) { + if (bmi < 18.5) { + return 'Underweight'; + } else if (bmi >= 18.5 && bmi < 25) { + return 'Normal'; + } else if (bmi >= 25 && bmi < 30) { + return 'Overweight'; + } else { + return 'Obese'; + } + } + + // BMR / Calories (shared logic) + void calculateBMRAndCalories( + {required String heightText, + required String weightText, + required String ageText, + required String heightUnit, + required String weightUnit, + required String gender, + required String activityLevel, + bool forCaloriesOnly = false}) { + if (heightText.trim().isEmpty || weightText.trim().isEmpty || ageText.trim().isEmpty) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double weight = double.parse(weightText.trim()); + int age = int.parse(ageText.trim()); + + if (age < 11 || age > 120) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + return; + } + + double heightInCm = _convertHeightToCm(height, heightUnit); + double weightInKg = _convertWeightToKg(weight, weightUnit); + + if (heightInCm <= 0 || weightInKg <= 0) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + return; + } + + double bmr = _calculateBMR(heightInCm, weightInKg, age, gender); + double activityMultiplier = _getActivityMultiplier(activityLevel); + double calories = bmr * activityMultiplier; + + bmrResult = bmr; + caloriesResult = calories; + + // ranges + if (bmr < 1200) { + bmrRange = 'Low'; + } else if (bmr < 1600) { + bmrRange = 'Normal'; + } else if (bmr < 2000) { + bmrRange = 'High'; + } else { + bmrRange = 'Very High'; + } + + if (calories < 1800) { + calorieRange = 'Low'; + } else if (calories < 2500) { + calorieRange = 'Normal'; + } else if (calories < 3200) { + calorieRange = 'High'; + } else { + calorieRange = 'High'; + } + + notifyListeners(); + } catch (e) { + bmrResult = null; + caloriesResult = null; + bmrRange = null; + calorieRange = null; + notifyListeners(); + } + } + + double _convertHeightToCm(double height, String unit) { + switch (unit) { + case 'cm': + return height; + case 'm': + return height * 100; + case 'ft': + return height * 30.48; + case 'in': + return height * 2.54; + default: + return height; + } + } + + double _calculateBMR(double heightCm, double weightKg, int age, String gender) { + if (gender == "Male") { + return 88.362 + (13.397 * weightKg) + (4.799 * heightCm) - (5.677 * age); + } else { + return 447.593 + (9.247 * weightKg) + (3.098 * heightCm) - (4.330 * age); + } + } + + double _getActivityMultiplier(String activityLevel) { + switch (activityLevel) { + case "Almost Inactive (no exercise)": + return 1.2; + case "Lightly active": + return 1.375; + case "Lightly active (1-3) days per week": + return 1.55; + case "Super active (very hard exercise)": + return 1.725; + default: + return 1.375; + } + } + + // IBW + void calculateIBW( + {required String heightText, + required String weightText, + required String heightUnit, + required String weightUnit, + required String bodyFrameSize}) { + if (heightText.trim().isEmpty || weightText.trim().isEmpty) { + ibwResult = null; + weightDifference = null; + weightStatus = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double weight = double.parse(weightText.trim()); + + double heightInCm = _convertHeightToCm(height, heightUnit); + double weightInKg = _convertWeightToKg(weight, weightUnit); + + if (heightInCm <= 0 || weightInKg <= 0) { + ibwResult = null; + weightDifference = null; + weightStatus = null; + notifyListeners(); + return; + } + + double ibw = _calculateIBWValue(heightInCm); + double adjusted = _applyFrameSizeAdjustment(ibw, bodyFrameSize); + double difference = weightInKg - adjusted; + String status = _getWeightStatus(difference); + + ibwResult = adjusted; + weightDifference = difference; + weightStatus = status; + notifyListeners(); + } catch (e) { + ibwResult = null; + weightDifference = null; + weightStatus = null; + notifyListeners(); + } + } + + double _calculateIBWValue(double heightCm) { + double baseHeight = 152.4; + double baseWeight = 50.0; + double heightMultiplier = 2.3; + + if (heightCm <= baseHeight) { + return baseWeight - ((baseHeight - heightCm) * 0.5); + } + + return baseWeight + ((heightCm - baseHeight) * heightMultiplier / 2.54); + } + + double _applyFrameSizeAdjustment(double ibw, String selectedBodyFrameSize) { + switch (selectedBodyFrameSize) { + case "Small (fingers overlaps)": + return ibw * 0.9; + case "Medium (fingers touch)": + return ibw; + case "Large (fingers don't touch)": + return ibw * 1.1; + default: + return ibw; + } + } + + String _getWeightStatus(double difference) { + if (difference.abs() < 2) { + return 'Normal'; + } else if (difference > 0) { + return 'Overweight'; + } else { + return 'Underweight'; + } + } + + // Body Fat + void calculateBodyFat( + {required String heightText, + required String neckText, + required String waistText, + required String hipText, + required String heightUnit, + required String neckUnit, + required String waistUnit, + required String hipUnit, + required String gender}) { + if (heightText.trim().isEmpty || neckText.trim().isEmpty || waistText.trim().isEmpty) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + return; + } + + if (gender == "Female" && hipText.trim().isEmpty) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + return; + } + + try { + double height = double.parse(heightText.trim()); + double neck = double.parse(neckText.trim()); + double waist = double.parse(waistText.trim()); + double hip = gender == "Female" ? double.parse(hipText.trim()) : 0; + + double heightInCm = _convertHeightToCm(height, heightUnit); + double neckInCm = _convertLengthToCm(neck, neckUnit); + double waistInCm = _convertLengthToCm(waist, waistUnit); + double hipInCm = gender == "Female" ? _convertLengthToCm(hip, hipUnit) : 0; + + if (heightInCm <= 0 || neckInCm <= 0 || waistInCm <= 0) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + return; + } + + double bodyFat = _calculateBodyFatPercentage(heightInCm, neckInCm, waistInCm, hipInCm, gender == "Male"); + String category = _getBodyFatCategory(bodyFat, gender == "Male"); + + bodyFatResult = bodyFat; + bodyFatCategory = category; + notifyListeners(); + } catch (e) { + bodyFatResult = null; + bodyFatCategory = null; + notifyListeners(); + } + } + + double _convertLengthToCm(double length, String unit) { + switch (unit) { + case 'cm': + return length; + case 'm': + return length * 100; + case 'ft': + return length * 30.48; + case 'in': + return length * 2.54; + default: + return length; + } + } + + double _calculateBodyFatPercentage(double heightCm, double neckCm, double waistCm, double hipCm, bool isMale) { + if (isMale) { + double waistMinusNeck = waistCm - neckCm; + if (waistMinusNeck <= 0) return 0; + double log10WaistNeck = _log10(waistMinusNeck); + double log10Height = _log10(heightCm); + double bodyDensity = 1.0324 - (0.19077 * log10WaistNeck) + (0.15456 * log10Height); + double bodyFat = (495 / bodyDensity) - 450; + return bodyFat.clamp(0, 100); + } else { + double waistHipMinusNeck = waistCm + hipCm - neckCm; + if (waistHipMinusNeck <= 0) return 0; + double log10WaistHipNeck = _log10(waistHipMinusNeck); + double log10Height = _log10(heightCm); + double bodyDensity = 1.29579 - (0.35004 * log10WaistHipNeck) + (0.22100 * log10Height); + double bodyFat = (495 / bodyDensity) - 450; + return bodyFat.clamp(0, 100); + } + } + + double _log10(double x) { + return (x <= 0) ? 0 : (log(x) / log(10)); + } + + String _getBodyFatCategory(double bodyFat, bool isMale) { + if (isMale) { + if (bodyFat < 6) return 'Essential Fat'; + if (bodyFat < 14) return 'Athletes'; + if (bodyFat < 18) return 'Fitness'; + if (bodyFat < 25) return 'Average'; + return 'Obese'; + } else { + if (bodyFat < 14) return 'Essential Fat'; + if (bodyFat < 21) return 'Athletes'; + if (bodyFat < 25) return 'Fitness'; + if (bodyFat < 32) return 'Average'; + return 'Obese'; + } + } + + // Crabs / Macros + void calculateMacros({required String caloriesText, required String selectedDietType}) { + if (caloriesText.trim().isEmpty) { + carbsCalories = null; + proteinCalories = null; + fatCalories = null; + carbsGrams = null; + proteinGrams = null; + fatGrams = null; + dietType = null; + notifyListeners(); + return; + } + + try { + double totalCalories = double.parse(caloriesText.trim()); + if (totalCalories <= 0) { + carbsCalories = null; + proteinCalories = null; + fatCalories = null; + carbsGrams = null; + proteinGrams = null; + fatGrams = null; + dietType = null; + notifyListeners(); + return; + } + + Map macroPercentages = _getMacroPercentages(selectedDietType); + double carbsPercent = macroPercentages['carbs']!; + double proteinPercent = macroPercentages['protein']!; + double fatPercent = macroPercentages['fat']!; + + double cCalories = totalCalories * (carbsPercent / 100); + double pCalories = totalCalories * (proteinPercent / 100); + double fCalories = totalCalories * (fatPercent / 100); + + double cGrams = cCalories / 4; + double pGrams = pCalories / 4; + double fGrams = fCalories / 9; + + carbsCalories = cCalories; + proteinCalories = pCalories; + fatCalories = fCalories; + carbsGrams = cGrams; + proteinGrams = pGrams; + fatGrams = fGrams; + dietType = selectedDietType; + notifyListeners(); + } catch (e) { + carbsCalories = null; + proteinCalories = null; + fatCalories = null; + carbsGrams = null; + proteinGrams = null; + fatGrams = null; + dietType = null; + notifyListeners(); + } + } + + Map _getMacroPercentages(String dietType) { + switch (dietType) { + case "Very Low Crabs": + return {'carbs': 5, 'protein': 30, 'fat': 65}; + case "Low Crabs": + return {'carbs': 20, 'protein': 35, 'fat': 45}; + case "Moderate Crabs": + return {'carbs': 40, 'protein': 30, 'fat': 30}; + case "USDA Guidelines ": + return {'carbs': 55, 'protein': 15, 'fat': 30}; + case "Zone Diet": + return {'carbs': 40, 'protein': 30, 'fat': 30}; + default: + return {'carbs': 40, 'protein': 30, 'fat': 30}; + } + } + + // Ovulation + DateTime? _parseCustomDate(String dateString) { + try { + List parts = dateString.split(' '); + if (parts.length >= 3) { + int day = int.parse(parts[0]); + String month = parts[1].replaceAll(',', ''); + int year = int.parse(parts[2]); + + Map months = { + 'January': 1, + 'February': 2, + 'March': 3, + 'April': 4, + 'May': 5, + 'June': 6, + 'July': 7, + 'August': 8, + 'September': 9, + 'October': 10, + 'November': 11, + 'December': 12, + 'Jan': 1, + 'Feb': 2, + 'Mar': 3, + 'Apr': 4, + 'Jun': 6, + 'Jul': 7, + 'Aug': 8, + 'Sep': 9, + 'Oct': 10, + 'Nov': 11, + 'Dec': 12, + }; + + int? monthNum = months[month]; + if (monthNum != null) { + return DateTime(year, monthNum, day); + } + } + return null; + } catch (e) { + return null; + } + } + + DateTime? _tryParseDate(String dateString) { + try { + // try ISO like + return DateTime.tryParse(dateString) ?? _parseCustomDate(dateString); + } catch (e) { + return null; + } + } + + String _formatDate(DateTime? date) { + if (date == null) return ''; + String day = date.day.toString().padLeft(2, '0'); + return '$day ${_getMonthName(date.month)}, ${date.year}'; + } + + String _getMonthName(int month) { + const List months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + return months[month - 1]; + } + + String _getDayName(int weekday) { + // DateTime.weekday returns 1 for Monday and 7 for Sunday. + const List days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + return days[weekday - 1]; + } + + String _formatDateWithDayName(DateTime? date) { + if (date == null) return ''; + String dayName = _getDayName(date.weekday); // Gets 'Monday', 'Tuesday', etc. + return dayName; + } + + Map convertBloodSugar(double value, String fromUnit) { + if (fromUnit == 'mg/dL') { + final mmolL = value / 18.0182; + return {'mgdL': value, 'mmolL': mmolL}; + } else if (fromUnit == 'mmol/L') { + final mgdL = value * 18.0182; + return {'mgdL': mgdL, 'mmolL': value}; + } + return {'mgdL': 0, 'mmolL': 0}; + } + + String _determineCyclePhase(int daysSincePeriod, int follicularPhase, int cycleLength) { + int normalizedDay = daysSincePeriod % cycleLength; + + if (normalizedDay < follicularPhase - 1) { + return 'Follicular Phase'; + } else if (normalizedDay < follicularPhase + 2) { + return 'Ovulation'; + } else if (normalizedDay < cycleLength) { + return 'Luteal Phase'; + } else { + return 'Menstruation'; + } + } + + void calculateOvulation({required String dateText, required String cycleLengthText, required String lutealPhaseText}) { + if (dateText.trim().isEmpty || cycleLengthText.trim().isEmpty || lutealPhaseText.trim().isEmpty) { + ovulationResult = null; + notifyListeners(); + return; + } + + DateTime? parsedDate = _tryParseDate(dateText); + if (parsedDate == null) { + ovulationResult = null; + notifyListeners(); + return; + } + + try { + int cycleLength = int.parse(cycleLengthText.trim()); + int lutealPhase = int.parse(lutealPhaseText.trim()); + + if (cycleLength < 16 || cycleLength > 40 || lutealPhase < 8 || lutealPhase > 16) { + ovulationResult = null; + notifyListeners(); + return; + } + + if (cycleLength <= lutealPhase) { + ovulationResult = null; + notifyListeners(); + return; + } + + int follicularPhase = cycleLength - lutealPhase; + DateTime ovulationDate = parsedDate.add(Duration(days: follicularPhase - 1)); + DateTime fertileStart = ovulationDate.subtract(Duration(days: 5)); + DateTime fertileEnd = ovulationDate.add(Duration(days: 1)); + int daysSincePeriod = DateTime.now().difference(parsedDate).inDays; + String cyclePhase = _determineCyclePhase(daysSincePeriod, follicularPhase, cycleLength); + + ovulationResult = { + 'lastPeriodDay': _formatDateWithDayName(parsedDate), + 'ovulationDay': _formatDateWithDayName(ovulationDate), + 'lastPeriodDate': _formatDate(parsedDate), + 'ovulationDate': _formatDate(ovulationDate), + 'fertileWindowStart': _formatDate(fertileStart), + 'fertileWindowEnd': _formatDate(fertileEnd), + 'cycleLength': cycleLength, + 'lutealPhase': lutealPhase, + 'follicularPhase': follicularPhase, + 'cyclePhase': cyclePhase, + 'nextPeriodDate': _formatDate(parsedDate.add(Duration(days: cycleLength))), + }; + + notifyListeners(); + } catch (e) { + ovulationResult = null; + notifyListeners(); + } + } + + // Delivery due date + void calculateDueDate({required String isoDateString}) { + if (isoDateString.trim().isEmpty) { + deliveryResult = null; + notifyListeners(); + return; + } + + DateTime? lmp; + try { + lmp = DateTime.tryParse(isoDateString) ?? _tryParseDate(isoDateString); + } catch (_) { + lmp = null; + } + + if (lmp == null) { + deliveryResult = null; + notifyListeners(); + return; + } + + // Pregnancy constants + const int totalPregnancyDays = 280; + + // Calculate main dates + final DateTime dueDate = lmp.add(const Duration(days: totalPregnancyDays)); + + // Trimester calculations + final DateTime firstTrimesterStart = lmp; + final DateTime firstTrimesterEnd = lmp.add(const Duration(days: 83)); + + final DateTime secondTrimesterStart = firstTrimesterEnd.add(const Duration(days: 1)); + final DateTime secondTrimesterEnd = lmp.add(const Duration(days: 195)); + + final DateTime thirdTrimesterStart = secondTrimesterEnd.add(const Duration(days: 1)); + final DateTime thirdTrimesterEnd = dueDate; + + deliveryResult = { + 'lmpDate': _formatDate(lmp), + 'dueDate': _formatDate(dueDate), + // Raw DateTime (useful for logic/UI) + 'lmpDateTime': lmp, + 'dueDateTime': dueDate, + 'dueDateDay': _formatDateWithDayName(dueDate), + // Trimester info + 'firstTrimester': { + 'start': _formatDate(firstTrimesterStart), + 'end': _formatDate(firstTrimesterEnd), + 'weeks': '1–12', + 'startDateTime': firstTrimesterStart, + 'endDateTime': firstTrimesterEnd, + }, + 'secondTrimester': { + 'start': _formatDate(secondTrimesterStart), + 'end': _formatDate(secondTrimesterEnd), + 'weeks': '13–27', + 'startDateTime': secondTrimesterStart, + 'endDateTime': secondTrimesterEnd, + }, + 'thirdTrimester': { + 'start': _formatDate(thirdTrimesterStart), + 'end': _formatDate(thirdTrimesterEnd), + 'weeks': '28–40', + 'startDateTime': thirdTrimesterStart, + 'endDateTime': thirdTrimesterEnd, + }, + }; + + notifyListeners(); + } + + // Blood sugar conversions + void calculateBloodSugar({required String valueText, required String unit}) { + if (valueText.trim().isEmpty) { + bloodSugarResult = null; + notifyListeners(); + return; + } + + try { + double value = double.parse(valueText.trim()); + + if (value <= 0) { + bloodSugarResult = null; + notifyListeners(); + return; + } + + double converted; + String convertedUnit; + + if (unit == 'mg/dL' || unit == 'MG/DL' || unit.toLowerCase().contains('mg')) { + // mg/dL -> mmol/L (glucose): divide by 18.0182 + converted = value / 18.0182; + convertedUnit = 'mmol/L'; + } else { + // mmol/L -> mg/dL + converted = value * 18.0182; + convertedUnit = 'mg/dL'; + } + + // round to sensible precision + double convertedRounded = double.parse(converted.toStringAsFixed(2)); + + bloodSugarResult = { + 'input': value, + 'inputUnit': unit, + 'converted': convertedRounded, + 'convertedUnit': convertedUnit, + }; + notifyListeners(); + } catch (e) { + bloodSugarResult = null; + notifyListeners(); + } + } + + // expose map-like results for widgets to forward to parent + Map? get bmiResultMap => bmiResult == null ? null : {'bmiResult': bmiResult, 'bmiCategory': bmiCategory}; + + Map? get caloriesResultMap => + caloriesResult == null ? null : {'calories': caloriesResult, 'bmr': bmrResult, 'calorieRange': calorieRange, 'bmrRange': bmrRange}; + + Map? get ibwResultMap => ibwResult == null ? null : {'ibw': ibwResult, 'difference': weightDifference, 'status': weightStatus}; + + Map? get bodyFatResultMap => bodyFatResult == null ? null : {'fatPercentage': bodyFatResult, 'fatCategory': bodyFatCategory}; + + Map? get macrosResultMap => carbsCalories == null + ? null + : { + 'totalCalories': carbsCalories! + proteinCalories! + fatCalories!, + 'carbsGrams': carbsGrams, + 'carbsCalories': carbsCalories, + 'proteinGrams': proteinGrams, + 'proteinCalories': proteinCalories, + 'fatGrams': fatGrams, + 'fatCalories': fatCalories, + 'dietType': dietType + }; + + void onBloodSugarChanged(String value, String fromUnit) { + _activeUnit = fromUnit; + + if (value.isEmpty) { + _mgdlValue = null; + _mmolValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + if (fromUnit == 'mg/dL') { + _mgdlValue = value; + _mmolValue = (parsed / 18.0182).toStringAsFixed(3); + } else { + _mmolValue = value; + _mgdlValue = (parsed * 18.0182).toStringAsFixed(3); + } + + notifyListeners(); + } + + void onBloodSugarMgdlChanged(String value) { + _mgdlValue = value; + _originalValue = value; + _originalUnit = 'mg/dL'; + _isSwapped = false; + _activeUnit = 'mg/dL'; + + if (value.isEmpty) { + _mmolValue = null; + _originalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mg/dL to mmol/L + _mmolValue = (parsed / 18.0182).toStringAsFixed(3); + notifyListeners(); + } + + void onBloodSugarMmolChanged(String value) { + _mmolValue = value; + _originalValue = value; + _originalUnit = 'mmol/L'; + _isSwapped = false; + _activeUnit = 'mmol/L'; + + if (value.isEmpty) { + _mgdlValue = null; + _originalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mmol/L to mg/dL + _mgdlValue = (parsed * 18.0182).toStringAsFixed(3); + notifyListeners(); + } + + void switchBloodSugarValues() { + // Toggle between two states using the original entered value + if (_originalValue == null || _originalValue!.isEmpty) return; + + final originalParsed = double.tryParse(_originalValue!); + if (originalParsed == null) return; + + _isSwapped = !_isSwapped; + + if (_originalUnit == 'mg/dL') { + if (_isSwapped) { + // Original was in mg/dL, now treat it as mmol/L + _mmolValue = _originalValue; // Keep original format + _mgdlValue = (originalParsed * 18.0182).toStringAsFixed(3); + } else { + // Back to original: value in mg/dL + _mgdlValue = _originalValue; // Keep original format + _mmolValue = (originalParsed / 18.0182).toStringAsFixed(3); + } + } else { + // Original was in mmol/L + if (_isSwapped) { + // Now treat it as mg/dL + _mgdlValue = _originalValue; // Keep original format + _mmolValue = (originalParsed / 18.0182).toStringAsFixed(3); + } else { + // Back to original: value in mmol/L + _mmolValue = _originalValue; // Keep original format + _mgdlValue = (originalParsed * 18.0182).toStringAsFixed(3); + } + } + + notifyListeners(); + } + + // --- NEW: Method to clear the values --- + void clearBloodSugar() { + _mgdlValue = null; + _mmolValue = null; + _originalValue = null; + _originalUnit = 'mg/dL'; + _isSwapped = false; + _activeUnit = 'mg/dL'; + notifyListeners(); + } + + void onBloodCholesterolChanged(String value, String fromUnit) { + _cholActiveUnit = fromUnit; + + if (value.isEmpty) { + _cholMgdlValue = null; + _cholMmolValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + if (fromUnit == 'mg/dL') { + _cholMgdlValue = value; + _cholMmolValue = (parsed / 38.67).toStringAsFixed(3); + } else { + _cholMmolValue = value; + _cholMgdlValue = (parsed * 38.67).toStringAsFixed(3); + } + + notifyListeners(); + } + + void onCholesterolMgdlChanged(String value) { + _cholMgdlValue = value; + _cholOriginalValue = value; + _cholOriginalUnit = 'mg/dL'; + _cholIsSwapped = false; + _cholActiveUnit = 'mg/dL'; + + if (value.isEmpty) { + _cholMmolValue = null; + _cholOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mg/dL to mmol/L + _cholMmolValue = (parsed / 38.67).toStringAsFixed(3); + notifyListeners(); + } + + void onCholesterolMmolChanged(String value) { + _cholMmolValue = value; + _cholOriginalValue = value; + _cholOriginalUnit = 'mmol/L'; + _cholIsSwapped = false; + _cholActiveUnit = 'mmol/L'; + + if (value.isEmpty) { + _cholMgdlValue = null; + _cholOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mmol/L to mg/dL + _cholMgdlValue = (parsed * 38.67).toStringAsFixed(3); + notifyListeners(); + } + + void switchBloodCholesterolValues() { + // Toggle between two states using the original entered value + if (_cholOriginalValue == null || _cholOriginalValue!.isEmpty) return; + + final originalParsed = double.tryParse(_cholOriginalValue!); + if (originalParsed == null) return; + + _cholIsSwapped = !_cholIsSwapped; + + if (_cholOriginalUnit == 'mg/dL') { + if (_cholIsSwapped) { + // Original was in mg/dL, now treat it as mmol/L + _cholMmolValue = _cholOriginalValue; // Keep original format + _cholMgdlValue = (originalParsed * 38.67).toStringAsFixed(3); + } else { + // Back to original: value in mg/dL + _cholMgdlValue = _cholOriginalValue; // Keep original format + _cholMmolValue = (originalParsed / 38.67).toStringAsFixed(3); + } + } else { + // Original was in mmol/L + if (_cholIsSwapped) { + // Now treat it as mg/dL + _cholMgdlValue = _cholOriginalValue; // Keep original format + _cholMmolValue = (originalParsed / 38.67).toStringAsFixed(3); + } else { + // Back to original: value in mmol/L + _cholMmolValue = _cholOriginalValue; // Keep original format + _cholMgdlValue = (originalParsed * 38.67).toStringAsFixed(3); + } + } + + notifyListeners(); + } + + void clearBloodCholesterol() { + _cholMgdlValue = null; + _cholMmolValue = null; + _cholOriginalValue = null; + _cholOriginalUnit = 'mg/dL'; + _cholIsSwapped = false; + _cholActiveUnit = 'mg/dL'; + notifyListeners(); + } + + void onTriglyceridesChanged(String value, String fromUnit) { + _triActiveUnit = fromUnit; + + if (value.isEmpty) { + _triMgdlValue = null; + _triMmolValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + if (fromUnit == 'mg/dL') { + _triMgdlValue = value; + _triMmolValue = (parsed / 88.57).toStringAsFixed(3); + } else { + _triMmolValue = value; + _triMgdlValue = (parsed * 88.57).toStringAsFixed(3); + } + + notifyListeners(); + } + + void onTriglyceridesMgdlChanged(String value) { + _triMgdlValue = value; + _triOriginalValue = value; + _triOriginalUnit = 'mg/dL'; + _triIsSwapped = false; + _triActiveUnit = 'mg/dL'; + + if (value.isEmpty) { + _triMmolValue = null; + _triOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mg/dL to mmol/L + _triMmolValue = (parsed / 88.57).toStringAsFixed(3); + notifyListeners(); + } + + void onTriglyceridesMmolChanged(String value) { + _triMmolValue = value; + _triOriginalValue = value; + _triOriginalUnit = 'mmol/L'; + _triIsSwapped = false; + _triActiveUnit = 'mmol/L'; + + if (value.isEmpty) { + _triMgdlValue = null; + _triOriginalValue = null; + notifyListeners(); + return; + } + + final parsed = double.tryParse(value); + if (parsed == null) return; + + // Convert mmol/L to mg/dL + _triMgdlValue = (parsed * 88.57).toStringAsFixed(3); + notifyListeners(); + } + + void switchTriglyceridesValues() { + // Toggle between two states using the original entered value + if (_triOriginalValue == null || _triOriginalValue!.isEmpty) return; + + final originalParsed = double.tryParse(_triOriginalValue!); + if (originalParsed == null) return; + + _triIsSwapped = !_triIsSwapped; + + if (_triOriginalUnit == 'mg/dL') { + if (_triIsSwapped) { + // Original was in mg/dL, now treat it as mmol/L + _triMmolValue = _triOriginalValue; // Keep original format + _triMgdlValue = (originalParsed * 88.57).toStringAsFixed(3); + } else { + // Back to original: value in mg/dL + _triMgdlValue = _triOriginalValue; // Keep original format + _triMmolValue = (originalParsed / 88.57).toStringAsFixed(3); + } + } else { + // Original was in mmol/L + if (_triIsSwapped) { + // Now treat it as mg/dL + _triMgdlValue = _triOriginalValue; // Keep original format + _triMmolValue = (originalParsed / 88.57).toStringAsFixed(3); + } else { + // Back to original: value in mmol/L + _triMmolValue = _triOriginalValue; // Keep original format + _triMgdlValue = (originalParsed * 88.57).toStringAsFixed(3); + } + } + + notifyListeners(); + } + + void clearTriglycerides() { + _triMgdlValue = null; + _triMmolValue = null; + _triOriginalValue = null; + _triOriginalUnit = 'mg/dL'; + _triIsSwapped = false; + _triActiveUnit = 'mg/dL'; + notifyListeners(); + } +} diff --git a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart new file mode 100644 index 0000000..faf97a1 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart @@ -0,0 +1,259 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/widgets/health_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + +class HealthCalculatorsPage extends StatefulWidget { + HealthCalConEnum type; + + HealthCalculatorsPage({super.key, required this.type}); + + @override + State createState() => _HealthCalculatorsPageState(); +} + +class _HealthCalculatorsPageState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt.get(); + return CollapsingListView( + title: widget.type == HealthCalConEnum.calculator ? "Health Calculators".needTranslation : "Health Converters".needTranslation, + child: widget.type == HealthCalConEnum.calculator + ? Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "General Health".needTranslation.toText16(weight: FontWeight.w600), + "Related To BMI, calories, body fat, etc to stay updated with your health.".needTranslation.toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox( + width: 12.w, + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Calculator".needTranslation, + message: "", + child: showCalculatorsItems(type: HealthCalculatorEnum.general), + onOkPressed: () {}, + ); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Women's Health".needTranslation.toText16(weight: FontWeight.w600), + "Related To periods, ovulation, pregnancy, and other topics.".needTranslation.toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Calculator".needTranslation, + message: "", + child: showCalculatorsItems(type: HealthCalculatorEnum.women), + onOkPressed: () {}, + ); + }), + ], + ).paddingSymmetrical(20.w, 24.h) + : Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bloodSugar, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Blood Sugar".needTranslation.toText16(weight: FontWeight.w600), + "Track your glucose levels, understand trends, and get personalized insights for better health.".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: Color(0xFF8F9AA3), + ) + ], + ), + ), + SizedBox( + width: 12.w, + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.bloodSugar), + ), + ); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.bloodCholestrol, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Blood Cholesterol".needTranslation.toText16(weight: FontWeight.w600), + "Monitor your cholesterol levels, track your LDL, HDL, and triglycerides. Get personalized recommendations for a healthy heart." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.bloodCholesterol), + ), + ); + }), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.triglycerides, height: 40.h, width: 40.w, fit: BoxFit.none), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + "Triglycerides Fat Blood".needTranslation.toText16(weight: FontWeight.w600), + "Manage triglycerides, a key blood fat. Understand levels, diet impacts, and heart health strategies." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: Color(0xFF8F9AA3)) + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + ], + ).paddingAll(16.w)) + .onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.triglycerides), + ), + ); + }), + ], + ).paddingSymmetrical(20.w, 24.h)); + } + + Widget showCalculatorsItems({required HealthCalculatorEnum type}) { + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, // 4 icons per row + crossAxisSpacing: 16.w, + mainAxisSpacing: 16.w, + childAspectRatio: 0.80), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: type == HealthCalculatorEnum.general ? generalHealthServices.length : womenHealthServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return HealthCard( + icon: type == HealthCalculatorEnum.general ? generalHealthServices[index].icon : womenHealthServices[index].icon, + labelText: type == HealthCalculatorEnum.general ? generalHealthServices[index].title : womenHealthServices[index].title, + onTap: () { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage( + calculatorType: type == HealthCalculatorEnum.general ? generalHealthServices[index].type : womenHealthServices[index].type, + clinicID: type == HealthCalculatorEnum.general ? generalHealthServices[index].clinicID : womenHealthServices[index].clinicID, + calculationID: type == HealthCalculatorEnum.general ? generalHealthServices[index].calculationID : womenHealthServices[index].calculationID, + ), + ), + ); + }, + ); + }, + ); + } + + final List generalHealthServices = [ + HealthComponentModel(title: "BMI\nCalculator".needTranslation, icon: AppAssets.bmi, type: HealthCalculatorsTypeEnum.bmi, clinicID: 108, calculationID: null), + HealthComponentModel(title: "Calories\nCalculator".needTranslation, icon: AppAssets.calories, type: HealthCalculatorsTypeEnum.calories, clinicID: null, calculationID: 2), + HealthComponentModel(title: "BMR\nCalculator".needTranslation, icon: AppAssets.bmr, type: HealthCalculatorsTypeEnum.bmr, clinicID: null, calculationID: 3), + HealthComponentModel(title: "Ideal Body\nWeight".needTranslation, icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.idealBodyWeight, clinicID: null, calculationID: 4), + HealthComponentModel(title: "Body Fat\nCalculator".needTranslation, icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.bodyFat, clinicID: null, calculationID: 5), + HealthComponentModel(title: "Carbs\nProtein & Fat".needTranslation, icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.crabsProteinFat, clinicID: null, calculationID: 11), + ]; + + final List womenHealthServices = [ + HealthComponentModel(title: "Ovulation\nPeriod".needTranslation, icon: AppAssets.locate_me, type: HealthCalculatorsTypeEnum.ovulation, clinicID: null, calculationID: 6 ), + HealthComponentModel(title: "Delivery\nDue Date".needTranslation, icon: AppAssets.activeCheck, type: HealthCalculatorsTypeEnum.deliveryDueDate, clinicID: null, calculationID: 6), + ]; +} + +class HealthComponentModel { + String title; + String? subTitle; + String icon; + Color? iconColor; + Color? bgColor; + Color? textColor; + HealthCalculatorsTypeEnum type; + int? clinicID; + int? calculationID; + + HealthComponentModel({required this.title, this.subTitle, required this.icon, this.iconColor, this.bgColor, this.textColor, required this.type, this.clinicID, this.calculationID}); +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/bf.dart b/lib/presentation/health_calculators_and_converts/widgets/bf.dart new file mode 100644 index 0000000..3a88eb3 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/bf.dart @@ -0,0 +1,641 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class BodyFatWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + BodyFatWidget({super.key, required this.onChange}); + + @override + _BodyFatWidgetState createState() => _BodyFatWidgetState(); +} + +class _BodyFatWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _neckController = TextEditingController(); + final TextEditingController _waistController = TextEditingController(); + final TextEditingController _hipController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + String selectedHeightUnit = 'cm'; + String selectedNeckUnit = 'in'; + String selectedWaistUnit = 'in'; + String selectedHipUnit = 'in'; + String selectedGender = "Male"; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _neckController.addListener(_onInputChanged); + _waistController.addListener(_onInputChanged); + _hipController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _neckController.removeListener(_onInputChanged); + _waistController.removeListener(_onInputChanged); + _hipController.removeListener(_onInputChanged); + _heightController.dispose(); + _neckController.dispose(); + _waistController.dispose(); + _hipController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBodyFat( + heightText: _heightController.text, + neckText: _neckController.text, + waistText: _waistController.text, + hipText: _hipController.text, + heightUnit: selectedHeightUnit, + neckUnit: selectedNeckUnit, + waistUnit: selectedWaistUnit, + hipUnit: selectedHipUnit, + gender: selectedGender, + ); + if (widget.onChange != null) widget.onChange!(provider.bodyFatResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Gender".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedGender.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _genders = ["Male", "Female"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Gender".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _genders.length, + itemBuilder: (context, index) { + final unit = _genders[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedGender, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedGender = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedGender = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Neck".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _neckController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '7', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedNeckUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedNeckUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedNeckUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedNeckUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Waist".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _waistController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '30', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWaistUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWaistUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWaistUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWaistUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Hip".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _hipController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '0', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHipUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHipUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHipUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Estimate the total body fat based on the size.".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart b/lib/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart new file mode 100644 index 0000000..e3bc7ff --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/blood_cholesterol.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BloodCholesterolWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + const BloodCholesterolWidget({super.key, this.onChange}); + + @override + State createState() => _BloodCholesterolWidgetState(); +} + +class _BloodCholesterolWidgetState extends State { + final TextEditingController _mgdlController = TextEditingController(); + final TextEditingController _mmolController = TextEditingController(); + final FocusNode _mgdlFocus = FocusNode(); + final FocusNode _mmolFocus = FocusNode(); + bool _isProgrammaticChange = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of( + context, + listen: false, + ).clearBloodCholesterol(); + }); + } + + @override + void dispose() { + _mgdlController.dispose(); + _mmolController.dispose(); + _mgdlFocus.dispose(); + _mmolFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + _isProgrammaticChange = true; + final mgdlText = provider.cholMgdlValue ?? ''; + final mmolText = provider.cholMmolValue ?? ''; + + // Only update if focus is not on this field (to avoid cursor jumping) + if (!_mgdlFocus.hasFocus && _mgdlController.text != mgdlText) { + _mgdlController.text = mgdlText; + _mgdlController.selection = TextSelection.fromPosition( + TextPosition(offset: _mgdlController.text.length), + ); + } + + if (!_mmolFocus.hasFocus && _mmolController.text != mmolText) { + _mmolController.text = mmolText; + _mmolController.selection = TextSelection.fromPosition( + TextPosition(offset: _mmolController.text.length), + ); + } + + _isProgrammaticChange = false; + + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildInputField( + label: "MG/DL", + hint: "120", + controller: _mgdlController, + focusNode: _mgdlFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onCholesterolMgdlChanged(value); + }, + ).paddingOnly(top: 16.h), + Row( + children: [ + const Expanded( + flex: 3, + child: Divider(height: 1, color: Color(0xFFEEEEEE)), + ), + SizedBox(width: 8.w), + Utils.buildSvgWithAssets( + icon: AppAssets.switchBtn, + width: 40.h, + height: 40.h, + ).onPress(() { + // Unfocus both fields before switching + _mgdlFocus.unfocus(); + _mmolFocus.unfocus(); + provider.switchBloodCholesterolValues(); + }), + ], + ), + _buildInputField( + label: "MMOL/L", + hint: "3.1", + controller: _mmolController, + focusNode: _mmolFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onCholesterolMmolChanged(value); + }, + ).paddingOnly(bottom: 16.h), + const Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Convert blood cholesterol values between mg/dL and mmol/L. ".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }, + ); + } + + /// Reusable Input Field + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + required FocusNode focusNode, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label.toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + SizedBox( + height: 40.h, + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + maxLines: 1, + onChanged: onChanged, + cursorHeight: 35.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 32.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.h, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/blood_sugar.dart b/lib/presentation/health_calculators_and_converts/widgets/blood_sugar.dart new file mode 100644 index 0000000..cf7cc61 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/blood_sugar.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BloodSugarWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + const BloodSugarWidget({super.key, this.onChange}); + + @override + State createState() => _BloodSugarWidgetState(); +} + +class _BloodSugarWidgetState extends State { + final TextEditingController _mgdlController = TextEditingController(); + final TextEditingController _mmolController = TextEditingController(); + final FocusNode _mgdlFocus = FocusNode(); + final FocusNode _mmolFocus = FocusNode(); + bool _isProgrammaticChange = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of( + context, + listen: false, + ).clearBloodSugar(); + }); + } + + @override + void dispose() { + _mgdlController.dispose(); + _mmolController.dispose(); + _mgdlFocus.dispose(); + _mmolFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + _isProgrammaticChange = true; + final mgdlText = provider.mgdlValue ?? ''; + final mmolText = provider.mmolValue ?? ''; + + // Only update if focus is not on this field (to avoid cursor jumping) + if (!_mgdlFocus.hasFocus && _mgdlController.text != mgdlText) { + _mgdlController.text = mgdlText; + _mgdlController.selection = TextSelection.fromPosition( + TextPosition(offset: _mgdlController.text.length), + ); + } + + if (!_mmolFocus.hasFocus && _mmolController.text != mmolText) { + _mmolController.text = mmolText; + _mmolController.selection = TextSelection.fromPosition( + TextPosition(offset: _mmolController.text.length), + ); + } + + _isProgrammaticChange = false; + + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + _buildInputField( + label: "MG/DL", + hint: "120", + controller: _mgdlController, + focusNode: _mgdlFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onBloodSugarMgdlChanged(value); + }, + ).paddingOnly(top: 16.h), + Row( + children: [ + const Expanded( + flex: 3, + child: Divider(height: 1, color: Color(0xFFEEEEEE)), + ), + SizedBox(width: 8.w), + Utils.buildSvgWithAssets( + icon: AppAssets.switchBtn, + width: 40.h, + height: 40.h, + ).onPress(() { + // Unfocus both fields before switching + _mgdlFocus.unfocus(); + _mmolFocus.unfocus(); + provider.switchBloodSugarValues(); + }), + ], + ), + _buildInputField( + label: "MMOL/L", + hint: "6.7", + controller: _mmolController, + focusNode: _mmolFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onBloodSugarMmolChanged(value); + }, + ).paddingOnly(bottom: 16.h), + const Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.globe, + width: 18.w, + height: 18.w, + ), + SizedBox(width: 12.w), + Expanded( + child: "Convert blood glucose values between mg/dL and mmol/L. (1 mmol/L ≈ 18.0182 mg/dL)".toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }, + ); + } + + /// Reusable Input Field + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + required FocusNode focusNode, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label.toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + SizedBox( + height: 40.h, + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + maxLines: 1, + onChanged: onChanged, + cursorHeight: 35.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 32.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.h, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/bmi.dart b/lib/presentation/health_calculators_and_converts/widgets/bmi.dart new file mode 100644 index 0000000..49b77c0 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/bmi.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BMIWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + BMIWidget({super.key, required this.onChange}); + + @override + _BMIWidgetState createState() => _BMIWidgetState(); +} + +class _BMIWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBMI(heightText: _heightController.text, weightText: _weightController.text, heightUnit: selectedHeightUnit, weightUnit: selectedWeightUnit); + // notify parent + if (widget.onChange != null) widget.onChange!(provider.bmiResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toUpperCase().toText12(fontWeight: FontWeight.w600, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + _onInputChanged(); + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toUpperCase().toText12(fontWeight: FontWeight.w600, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculate the BMI value and weight status to identify the healthy weight. NOt appropriate for children and women who are pregnant or breastfeeding." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/bmr.dart b/lib/presentation/health_calculators_and_converts/widgets/bmr.dart new file mode 100644 index 0000000..5f429ff --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/bmr.dart @@ -0,0 +1,522 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class BMRWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + BMRWidget({super.key, required this.onChange}); + + @override + _BMRWidgetState createState() => _BMRWidgetState(); +} + +class _BMRWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final TextEditingController _ageController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + String selectedActivityLevel = 'Lightly active'; + String selectedGender = "Male"; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + _ageController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _ageController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + _ageController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBMRAndCalories( + heightText: _heightController.text, + weightText: _weightController.text, + ageText: _ageController.text, + heightUnit: selectedHeightUnit, + weightUnit: selectedWeightUnit, + gender: selectedGender, + activityLevel: selectedActivityLevel, + ); + if (widget.onChange != null) widget.onChange!(provider.caloriesResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + AppState appState = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column(children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Gender".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedGender.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _genders = ["Male", "Female"]; + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Gender".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _genders.length, + itemBuilder: (context, index) { + final unit = _genders[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedGender, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + print(value); + setState(() { + selectedGender = value; + }); + _onInputChanged(); + Navigator.of(context).pop(); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedGender = unit; + }); + _onInputChanged(); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Age (11-120) yrs".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _ageController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '20', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Activity Level".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedActivityLevel.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Activity Level".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedActivityLevel, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedActivityLevel = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedActivityLevel = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculates daily calorie intake based on server factors, like height, weight, age, gender and daily physical activity." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ])); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/calories.dart b/lib/presentation/health_calculators_and_converts/widgets/calories.dart new file mode 100644 index 0000000..fbe15a6 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/calories.dart @@ -0,0 +1,523 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class CaloriesWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + CaloriesWidget({super.key, required this.onChange}); + + @override + _CaloriesWidgetState createState() => _CaloriesWidgetState(); +} + +class _CaloriesWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final TextEditingController _ageController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + String selectedActivityLevel = 'Lightly active'; + String selectedGender = "Male"; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + _ageController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _ageController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + _ageController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateBMRAndCalories( + heightText: _heightController.text, + weightText: _weightController.text, + ageText: _ageController.text, + heightUnit: selectedHeightUnit, + weightUnit: selectedWeightUnit, + gender: selectedGender, + activityLevel: selectedActivityLevel, + ); + if (widget.onChange != null) widget.onChange!(provider.caloriesResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + AppState appState = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Gender".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedGender.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _genders = ["Male", "Female"]; + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Gender".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _genders.length, + itemBuilder: (context, index) { + final unit = _genders[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedGender, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedGender = value; + }); + _onInputChanged(); + Navigator.of(context).pop(); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedGender = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Age (11-120) yrs".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _ageController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '20', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Activity Level".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedActivityLevel.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Almost Inactive (no exercise)", "Lightly active", "Lightly active (1-3) days per week", "Super active (very hard exercise)"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Activity Level".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedActivityLevel, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedActivityLevel = value; + }); + _onInputChanged(); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedActivityLevel = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculates daily calorie intake based on server factors, like height, weight, age, gender and daily physical activity." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/crabs.dart b/lib/presentation/health_calculators_and_converts/widgets/crabs.dart new file mode 100644 index 0000000..5fe1215 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/crabs.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_detailed_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class CrabsWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + CrabsWidget({super.key, required this.onChange}); + + @override + _CrabsWidgetState createState() => _CrabsWidgetState(); +} + +class _CrabsWidgetState extends State { + final TextEditingController _caloriesController = TextEditingController(); + String selectedDietType = 'Very Low Crabs'; + Function(String)? onUnitSelected; + + @override + void initState() { + super.initState(); + _caloriesController.addListener(_onInputChanged); + } + + @override + void dispose() { + _caloriesController.removeListener(_onInputChanged); + _caloriesController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateMacros(caloriesText: _caloriesController.text, selectedDietType: selectedDietType); + if (widget.onChange != null) widget.onChange!(provider.macrosResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Calories Per Day".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _caloriesController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '2000', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.ask_doctor_icon).onPress(() { + Navigator.pop(context); + Navigator.of(context).push(CustomPageRoute(page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.calories))); + }), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Diet Type".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedDietType.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Very Low Crabs", "Low Crabs", "Moderate Crabs", "USDA Guidelines ", "Zone Diet"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Diet Type".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedDietType, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedDietType = value; + _onInputChanged(); + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedDietType = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculate carbohydrate based protein and fat ration in calories and grams according to a pre set ratio." + .toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/dduedate.dart b/lib/presentation/health_calculators_and_converts/widgets/dduedate.dart new file mode 100644 index 0000000..15c0a0c --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/dduedate.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class DeliveryDueDWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + DeliveryDueDWidget({super.key, required this.onChange}); + + @override + _DeliveryDueDWidgetState createState() => _DeliveryDueDWidgetState(); +} + +class _DeliveryDueDWidgetState extends State { + final TextEditingController _date = TextEditingController(); + String selectedDate = ''; + Function(String)? onUnitSelected; + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 0.h, bottom: 0.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + TextInputWidget( + labelText: "Last Period Date", + hintText: "11 July, 1994".needTranslation, + controller: _date, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.birthday_cake, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + + setState(() { + selectedDate = Utils.formatDateToDisplay(val); + _date.text = selectedDate; + }); + provider.calculateDueDate(isoDateString: val); + + if (widget.onChange != null) { + widget.onChange!(provider.deliveryResult); + } + }, + ).withVerticalPadding(4.h), + ], + ), + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/health_card.dart b/lib/presentation/health_calculators_and_converts/widgets/health_card.dart new file mode 100644 index 0000000..d54d037 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/health_card.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HealthCard extends StatelessWidget { + const HealthCard({ + super.key, + required this.icon, + required this.labelText, + this.onTap, + }); + + final String icon; + final String labelText; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: icon, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).toShimmer2(isShow: false, radius: 12.r), + SizedBox(height: 14.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + child: labelText.toText12( + fontWeight: FontWeight.w600, + maxLine: 2, + )), + ], + ), + ], + ).paddingAll(16.w).onPress(() { + onTap!(); + }), + ); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/ibw.dart b/lib/presentation/health_calculators_and_converts/widgets/ibw.dart new file mode 100644 index 0000000..5f67511 --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/ibw.dart @@ -0,0 +1,393 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class IdealBodyWeightWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + IdealBodyWeightWidget({super.key, required this.onChange}); + + @override + _IdealBodyWeightWidgetState createState() => _IdealBodyWeightWidgetState(); +} + +class _IdealBodyWeightWidgetState extends State { + final TextEditingController _heightController = TextEditingController(); + final TextEditingController _weightController = TextEditingController(); + final List _heightUnits = ['cm', 'm', 'ft', 'in']; + final List _weightUnits = ['kg', 'lb']; + String selectedHeightUnit = 'cm'; + String selectedWeightUnit = 'kg'; + String selectedBodyFrameSize = 'Medium (fingers touch)'; + Function(String)? onUnitSelected; + + @override + void initState() { + super.initState(); + _heightController.addListener(_onInputChanged); + _weightController.addListener(_onInputChanged); + } + + @override + void dispose() { + _heightController.removeListener(_onInputChanged); + _weightController.removeListener(_onInputChanged); + _heightController.dispose(); + _weightController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + provider.calculateIBW( + heightText: _heightController.text, + weightText: _weightController.text, + heightUnit: selectedHeightUnit, + weightUnit: selectedWeightUnit, + bodyFrameSize: selectedBodyFrameSize, + ); + if (widget.onChange != null) widget.onChange!(provider.ibwResultMap); + } + + @override + Widget build(BuildContext context) { + DialogService dialogService = getIt(); + + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Height".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _heightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '175', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedHeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _heightUnits.length, + itemBuilder: (context, index) { + final unit = _heightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedHeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedHeightUnit = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedHeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Weight".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _weightController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '75', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + ) + ], + ), + ), + Container( + width: 1.w, + height: 30.w, + color: Color(0xFFEAEAEB), + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedWeightUnit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(() { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Unit".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _weightUnits.length, + itemBuilder: (context, index) { + final unit = _weightUnits[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedWeightUnit, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedWeightUnit = value; + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toUpperCase().toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedWeightUnit = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Body Frame Size".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + selectedBodyFrameSize.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ).paddingSymmetrical(0.w, 16.w).onPress(() { + List _activity = ["Small (fingers overlaps)", "Medium (fingers touch)", "Large (fingers don't touch)"]; + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Select Body Frame Size".needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: _activity.length, + itemBuilder: (context, index) { + final unit = _activity[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: 70, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: unit, + groupValue: selectedBodyFrameSize, + activeColor: AppColors.errorColor, + onChanged: (String? value) { + if (value == null) return; + setState(() { + selectedBodyFrameSize = value; + }); + Navigator.pop(context); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + unit.toCamelCase.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, + ], + ).onPress(() { + setState(() { + selectedBodyFrameSize = unit; + _onInputChanged(); + }); + Navigator.pop(context); + }), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider(height: 1, color: Color(0xFFEEEEEE)); + }, + ), + ), + onOkPressed: () {}, + ); + }), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculates the ideal body weight based on height, weight and body size.".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart b/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart new file mode 100644 index 0000000..de209cb --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class OvulationWidget extends StatefulWidget { + Function(dynamic result)? onChange; + + OvulationWidget({super.key, required this.onChange}); + + @override + _OvulationWidgetState createState() => _OvulationWidgetState(); +} + +class _OvulationWidgetState extends State { + final TextEditingController _ageController = TextEditingController(); + final TextEditingController _cycleLengthController = TextEditingController(); + final TextEditingController _lutealPhaseLengthController = TextEditingController(); + Function(String)? onUnitSelected; + String selectedDate = ''; + + @override + void initState() { + super.initState(); + _ageController.addListener(_onInputChanged); + _cycleLengthController.addListener(_onInputChanged); + _lutealPhaseLengthController.addListener(_onInputChanged); + } + + @override + void dispose() { + _ageController.removeListener(_onInputChanged); + _cycleLengthController.removeListener(_onInputChanged); + _lutealPhaseLengthController.removeListener(_onInputChanged); + _ageController.dispose(); + _cycleLengthController.dispose(); + _lutealPhaseLengthController.dispose(); + super.dispose(); + } + + void _onInputChanged() { + final provider = Provider.of(context, listen: false); + final dateText = selectedDate.isEmpty ? _ageController.text.trim() : selectedDate; + provider.calculateOvulation(dateText: dateText, cycleLengthText: _cycleLengthController.text, lutealPhaseText: _lutealPhaseLengthController.text); + if (widget.onChange != null) widget.onChange!(provider.ovulationResult); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, _) { + return Container( + margin: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + TextInputWidget( + labelText: "Date", + hintText: "11 July, 1994".needTranslation, + controller: _ageController, + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.birthday_cake, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + setState(() { + _ageController.text = selectedDate = Utils.formatDateToDisplay(val); + }); + _onInputChanged(); + }, + ).withVerticalPadding(8), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Average Cycle Length (Usually 28 days)".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _cycleLengthController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '28', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Divider(height: 1, color: Color(0xFFEEEEEE)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + SizedBox(width: 12.w), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Average Luteal Phase Length(Usually 14 days)".needTranslation.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: TextField( + controller: _lutealPhaseLengthController, + keyboardType: TextInputType.number, + maxLines: 1, + cursorHeight: 14, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: '14', + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.0), + ), + ) + ], + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.globe, width: 18.w, height: 18.w), + SizedBox(width: 12.w), + Expanded( + child: "Calculate ovulation and fertile window based on last period, cycle length and luteal phase.".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }); + } +} diff --git a/lib/presentation/health_calculators_and_converts/widgets/triglycerides.dart b/lib/presentation/health_calculators_and_converts/widgets/triglycerides.dart new file mode 100644 index 0000000..1c8c99e --- /dev/null +++ b/lib/presentation/health_calculators_and_converts/widgets/triglycerides.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; + +class TriglyceridesWidget extends StatefulWidget { + final Function(dynamic result)? onChange; + + const TriglyceridesWidget({super.key, this.onChange}); + + @override + State createState() => _TriglyceridesWidgetState(); +} + +class _TriglyceridesWidgetState extends State { + final TextEditingController _mgdlController = TextEditingController(); + final TextEditingController _mmolController = TextEditingController(); + final FocusNode _mgdlFocus = FocusNode(); + final FocusNode _mmolFocus = FocusNode(); + bool _isProgrammaticChange = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of( + context, + listen: false, + ).clearTriglycerides(); + }); + } + + @override + void dispose() { + _mgdlController.dispose(); + _mmolController.dispose(); + _mgdlFocus.dispose(); + _mmolFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + _isProgrammaticChange = true; + + final mgdlText = provider.triMgdlValue ?? ''; + final mmolText = provider.triMmolValue ?? ''; + + // Only update if focus is not on this field (to avoid cursor jumping) + if (!_mgdlFocus.hasFocus && _mgdlController.text != mgdlText) { + _mgdlController.text = mgdlText; + _mgdlController.selection = TextSelection.fromPosition( + TextPosition(offset: _mgdlController.text.length), + ); + } + + if (!_mmolFocus.hasFocus && _mmolController.text != mmolText) { + _mmolController.text = mmolText; + _mmolController.selection = TextSelection.fromPosition( + TextPosition(offset: _mmolController.text.length), + ); + } + + _isProgrammaticChange = false; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + _buildInputField( + label: "MG/DL", + hint: "150", + controller: _mgdlController, + focusNode: _mgdlFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onTriglyceridesMgdlChanged(value); + }, + ).paddingOnly(top: 16.h), + + Row( + children: [ + const Expanded( + flex: 3, + child: Divider(height: 1, color: Color(0xFFEEEEEE)), + ), + SizedBox(width: 8.w), + Utils.buildSvgWithAssets( + icon: AppAssets.switchBtn, + width: 40.h, + height: 40.h, + ).onPress(() { + // Unfocus both fields before switching + _mgdlFocus.unfocus(); + _mmolFocus.unfocus(); + provider.switchTriglyceridesValues(); + }), + ], + ), + + _buildInputField( + label: "MMOL/L", + hint: "1.7", + controller: _mmolController, + focusNode: _mmolFocus, + onChanged: (value) { + if (_isProgrammaticChange) return; + provider.onTriglyceridesMmolChanged(value); + }, + ).paddingOnly(bottom: 16.h), + + const Divider(height: 1, color: Color(0xFFEEEEEE)), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.globe, + width: 18.w, + height: 18.w, + ), + SizedBox(width: 12.w), + Expanded( + child: + "Convert triglyceride values between mg/dL and mmol/L." + .toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + ), + ], + ).paddingSymmetrical(0.w, 16.w), + ], + ), + ); + }, + ); + } + + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + required FocusNode focusNode, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label.toText12( + fontWeight: FontWeight.w500, + color: AppColors.inputLabelTextColor, + ), + SizedBox( + height: 40.h, + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + onChanged: onChanged, + cursorHeight: 35.h, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 32.f, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart new file mode 100644 index 0000000..56f82fc --- /dev/null +++ b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart @@ -0,0 +1,567 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class AddHealthTrackerEntryPage extends StatefulWidget { + final HealthTrackerTypeEnum trackerType; + + const AddHealthTrackerEntryPage({ + super.key, + required this.trackerType, + }); + + @override + State createState() => _AddHealthTrackerEntryPageState(); +} + +class _AddHealthTrackerEntryPageState extends State { + late DialogService dialogService; + + // Controllers for date and time + final TextEditingController dateController = TextEditingController(); + final TextEditingController timeController = TextEditingController(); + + @override + void initState() { + super.initState(); + dialogService = getIt.get(); + } + + @override + void dispose() { + dateController.dispose(); + timeController.dispose(); + super.dispose(); + } + + /// Get page title based on tracker type + String _getPageTitle() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Add Blood Sugar".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Add Blood Pressure".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Add Weight".needTranslation; + } + } + + /// Get success message based on tracker type + String _getSuccessMessage() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Blood Sugar Data saved successfully".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Blood Pressure Data saved successfully".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Weight Data saved successfully".needTranslation; + } + } + + /// Save entry based on tracker type + Future _saveEntry(HealthTrackersViewModel viewModel) async { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + await _saveBloodSugarEntry(viewModel); + break; + case HealthTrackerTypeEnum.bloodPressure: + await _saveBloodPressureEntry(viewModel); + break; + case HealthTrackerTypeEnum.weightTracker: + await _saveWeightEntry(viewModel); + break; + } + } + + // Save Blood Sugar entry + Future _saveBloodSugarEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveBloodSugarEntry( + dateTime: dateTime, + measureTime: viewModel.selectedBloodSugarMeasureTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Save Weight entry + Future _saveWeightEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveWeightEntry( + dateTime: dateTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Save Blood Pressure entry + Future _saveBloodPressureEntry(HealthTrackersViewModel viewModel) async { + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + // Combine date and time + final dateTime = "${dateController.text} ${timeController.text}"; + + // Call ViewModel method with callbacks + await viewModel.saveBloodPressureEntry( + dateTime: dateTime, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + _showSuccessAndPop(); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + } + + // Show success message and pop back + void _showSuccessAndPop() { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: _getSuccessMessage(), + ), + callBackFunc: () { + Navigator.pop(context); + }, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + // Reusable method to show selection bottom sheet + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + // Blood Sugar unit selection + void _showBloodSugarUnitSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.bloodSugarUnit, + selectedValue: viewModel.selectedBloodSugarUnit, + onSelected: viewModel.setBloodSugarUnit, + useUpperCase: false, + ); + } + + // Blood Sugar measure time selection + void _showBloodSugarEntryTimeBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Measure Time".needTranslation, + items: viewModel.bloodSugarMeasureTimeEnList, + selectedValue: viewModel.selectedBloodSugarMeasureTime, + onSelected: viewModel.setBloodSugarMeasureTime, + useUpperCase: false, + ); + } + + // Weight unit selection + void _showWeightUnitSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.weightUnits, + selectedValue: viewModel.selectedWeightUnit, + onSelected: viewModel.setWeightUnit, + useUpperCase: false, + ); + } + + // Blood Pressure measured arm selection + void _showMeasuredArmSelectionBottomSheet(BuildContext context, HealthTrackersViewModel viewModel) { + FocusScope.of(context).unfocus(); + _showSelectionBottomSheet( + context: context, + title: "Select Arm".needTranslation, + items: viewModel.measuredArmList, + selectedValue: viewModel.selectedMeasuredArm, + onSelected: viewModel.setMeasuredArm, + useUpperCase: false, + ); + } + + // Reusable method to build text field + Widget _buildTextField(TextEditingController controller, String hintText, {TextInputType keyboardType = TextInputType.name}) { + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: 1, + cursorHeight: 14.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hintText, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ); + } + + // Reusable method to build settings row + Widget _buildSettingsRow({ + required String icon, + required String label, + String? value, + Widget? inputField, + String? unit, + VoidCallback? onUnitTap, + VoidCallback? onRowTap, + Color? iconColor, + bool showDivider = true, + }) { + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 40.w, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.greyColor, + borderRadius: 10.r, + hasShadow: false, + ), + child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w, iconColor: iconColor)), + ), + SizedBox(width: 12.w), + Expanded( + flex: unit != null ? 2 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + if (inputField != null) + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: inputField, + ) + else if (value != null && value.isNotEmpty) + value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ), + if (unit != null) ...[ + Container( + width: 1.w, + height: 30.w, + color: AppColors.dividerColor, + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(onUnitTap ?? () {}), + ), + ] else if (onRowTap != null) ...[ + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 8.w), + ], + ], + ).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}), + if (showDivider) Divider(height: 1, color: AppColors.dividerColor), + ], + ); + } + + /// Build form fields based on tracker type + Widget _buildFormFields(HealthTrackersViewModel viewModel) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return _buildBloodSugarForm(viewModel); + case HealthTrackerTypeEnum.bloodPressure: + return _buildBloodPressureForm(viewModel); + case HealthTrackerTypeEnum.weightTracker: + return _buildWeightForm(viewModel); + } + } + + /// Blood Sugar form fields + Widget _buildBloodSugarForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.heightIcon, + label: "Enter Blood Sugar".needTranslation, + inputField: _buildTextField(viewModel.bloodSugarController, '', keyboardType: TextInputType.number), + unit: viewModel.selectedBloodSugarUnit, + onUnitTap: () => _showBloodSugarUnitSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + Divider(height: 1, color: AppColors.dividerColor), + _buildSettingsRow( + icon: AppAssets.weight_tracker_icon, + label: "Select Measure Time".needTranslation, + value: viewModel.selectedBloodSugarMeasureTime, + onRowTap: () => _showBloodSugarEntryTimeBottomSheet(context, viewModel), + ), + ], + ); + } + + /// Blood Pressure form fields + Widget _buildBloodPressureForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.bloodPressureIcon, + iconColor: AppColors.greyTextColor, + label: "Enter Systolic Value".needTranslation, + inputField: _buildTextField(viewModel.systolicController, '', keyboardType: TextInputType.number), + ), + _buildSettingsRow( + icon: AppAssets.bloodPressureIcon, + iconColor: AppColors.greyTextColor, + label: "Enter Diastolic Value".needTranslation, + inputField: _buildTextField(viewModel.diastolicController, '', keyboardType: TextInputType.number), + ), + _buildSettingsRow( + icon: AppAssets.bodyIcon, + iconColor: AppColors.greyTextColor, + label: "Select Arm".needTranslation, + value: viewModel.selectedMeasuredArm, + onRowTap: () => _showMeasuredArmSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + ], + ); + } + + /// Weight form fields + Widget _buildWeightForm(HealthTrackersViewModel viewModel) { + return Column( + children: [ + _buildSettingsRow( + icon: AppAssets.weightScale, + label: "Enter Weight".needTranslation, + inputField: _buildTextField(viewModel.weightController, '', keyboardType: TextInputType.number), + unit: viewModel.selectedWeightUnit, + onUnitTap: () => _showWeightUnitSelectionBottomSheet(context, viewModel), + ), + _buildDateTimeFields(), + ], + ); + } + + /// Common date and time fields + Widget _buildDateTimeFields() { + return Column( + children: [ + TextInputWidget( + controller: dateController, + isReadOnly: true, + isArrowTrailing: true, + labelText: "Date", + hintText: "Select date".needTranslation, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.calendarGrey, + selectionType: SelectionTypeEnum.calendar, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + try { + final parsedDate = DateTime.parse(val); + final formattedDate = DateFormat('dd MMM yyyy').format(parsedDate); + dateController.text = formattedDate; + log("date: $formattedDate"); + } catch (e) { + dateController.text = val; + log("date: $val"); + } + }, + ), + TextInputWidget( + controller: timeController, + isReadOnly: true, + isArrowTrailing: true, + labelText: "Time", + hintText: "Select time".needTranslation, + focusNode: FocusNode(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.calendarGrey, + selectionType: SelectionTypeEnum.time, + isHideSwitcher: true, + onCalendarTypeChanged: (val) {}, + onChange: (val) { + if (val == null) return; + timeController.text = val; + log("time: $val"); + }, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: _getPageTitle(), + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Save".needTranslation, + onPressed: () async => await _saveEntry(viewModel), + borderRadius: 12.r, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: _buildFormFields(viewModel), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/health_trackers/health_tracker_detail_page.dart b/lib/presentation/health_trackers/health_tracker_detail_page.dart new file mode 100644 index 0000000..9443f12 --- /dev/null +++ b/lib/presentation/health_trackers/health_tracker_detail_page.dart @@ -0,0 +1,1303 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/week_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/year_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/widgets/tracker_last_value_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class HealthTrackerDetailPage extends StatefulWidget { + final HealthTrackerTypeEnum trackerType; + + const HealthTrackerDetailPage({super.key, required this.trackerType}); + + @override + State createState() => _HealthTrackerDetailPageState(); +} + +class _HealthTrackerDetailPageState extends State { + @override + void initState() { + super.initState(); + // Load data based on tracker type + WidgetsBinding.instance.addPostFrameCallback((_) async { + final viewModel = context.read(); + await _loadTrackerData(viewModel); + }); + } + + /// Load data based on tracker type + Future _loadTrackerData(HealthTrackersViewModel viewModel) async { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + await viewModel.getBloodSugar(); + break; + case HealthTrackerTypeEnum.bloodPressure: + await viewModel.getBloodPressure(); + break; + case HealthTrackerTypeEnum.weightTracker: + await viewModel.getWeight(); + break; + } + } + + /// Get page title based on tracker type + String _getPageTitle() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Blood Sugar".needTranslation; + case HealthTrackerTypeEnum.bloodPressure: + return "Blood Pressure".needTranslation; + case HealthTrackerTypeEnum.weightTracker: + return "Weight".needTranslation; + } + } + + /// Get unit based on tracker type + String _getUnit() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return 'mg/dL'; + case HealthTrackerTypeEnum.bloodPressure: + return 'mmHg'; + case HealthTrackerTypeEnum.weightTracker: + return 'kg'; + } + } + + /// Get empty state message based on tracker type + String _getEmptyStateMessage() { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return "Please add data to track your Blood Sugar"; + case HealthTrackerTypeEnum.bloodPressure: + return "Please add data to track your Blood Pressure"; + case HealthTrackerTypeEnum.weightTracker: + return "Please add data to track your Weight"; + } + } + + /// Check if data is empty based on tracker type + bool _hasNoData(HealthTrackersViewModel viewModel) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return viewModel.weekDiabeticPatientResult.isEmpty && + viewModel.monthDiabeticPatientResult.isEmpty && + viewModel.yearDiabeticPatientResult.isEmpty; + case HealthTrackerTypeEnum.bloodPressure: + return viewModel.weekBloodPressureResult.isEmpty && viewModel.monthBloodPressureResult.isEmpty && viewModel.yearBloodPressureResult.isEmpty; + case HealthTrackerTypeEnum.weightTracker: + return viewModel.weekWeightMeasurementResult.isEmpty && + viewModel.monthWeightMeasurementResult.isEmpty && + viewModel.yearWeightMeasurementResult.isEmpty; + } + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + final dialogService = getIt.get(); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + void _showHistoryDurationBottomsheet(BuildContext context, HealthTrackersViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Duration".needTranslation, + items: viewModel.durationFilters, + selectedValue: viewModel.selectedDurationFilter, + onSelected: viewModel.setFilterDuration, + ); + } + + Widget buildHistoryListTile({required String title, required String subTitle, required String measureDesc, double? value}) { + // Get status color and rotation based on value and tracker type + Color statusColor = AppColors.successColor; + double rotation = 0; + + if (value != null) { + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + if (value < 70) { + statusColor = AppColors.errorColor; + rotation = 0; // pointing down + } else if (value <= 100) { + statusColor = AppColors.successColor; + rotation = -3.14159 / 2; // pointing right + } else if (value <= 125) { + statusColor = AppColors.ratingColorYellow; + rotation = 3.14159; // pointing up + } else { + statusColor = AppColors.errorColor; + rotation = 3.14159; // pointing up + } + break; + case HealthTrackerTypeEnum.bloodPressure: + // Systolic pressure ranges + if (value < 90) { + statusColor = AppColors.errorColor; + rotation = 0; // Low - pointing down + } else if (value <= 120) { + statusColor = AppColors.successColor; + rotation = -3.14159 / 2; // Normal - pointing right + } else if (value <= 140) { + statusColor = AppColors.ratingColorYellow; + rotation = 3.14159; // Elevated - pointing up + } else { + statusColor = AppColors.errorColor; + rotation = 3.14159; // High - pointing up + } + break; + case HealthTrackerTypeEnum.weightTracker: + // Weight doesn't have good/bad indicators, just show neutral + statusColor = AppColors.transparent; + rotation = -3.14159 / 2; // pointing right (neutral) + break; + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AppCustomChipWidget(labelText: title), + if (measureDesc.isNotEmpty) ...[ + SizedBox(width: 8.w), + AppCustomChipWidget(labelText: measureDesc), + ], + ], + ), + SizedBox(height: 4.h), + subTitle.toText16(weight: FontWeight.w600, color: AppColors.textColor), + ], + ), + Transform.rotate( + angle: rotation, + child: Utils.buildSvgWithAssets( + icon: AppAssets.lowIndicatorIcon, + iconColor: statusColor, + height: 20.h, + width: 20.h, + ), + ), + ], + ).paddingSymmetrical(0, 8.h); + } + + Widget _buildHistoryGraphOrList() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Consumer(builder: (BuildContext context, HealthTrackersViewModel viewModel, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + "History".needTranslation.toText16(isBold: true), + if (viewModel.isGraphView) ...[ + SizedBox(width: 12.w), + InkWell( + onTap: () => _showHistoryDurationBottomsheet(context, viewModel), + child: Container( + padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + backgroundColor: AppColors.greyColor, + borderRadius: 8.r, + hasShadow: true, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + viewModel.selectedDurationFilter.toText12(fontWeight: FontWeight.w500), + SizedBox(width: 4.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 16.h), + ], + ), + ), + ), + ], + ], + ), + InkWell( + onTap: () => viewModel.setGraphView(!viewModel.isGraphView), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(viewModel.isGraphView), + child: Utils.buildSvgWithAssets( + icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, + height: 24.h, + width: 24.h, + ), + ), + ), + ), + ], + ), + if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else ...[SizedBox(height: 16.h), _buildHistoryGraph()] + ], + ); + }), + ); + } + + String _formatTime(DateTime time) { + final hour = time.hour; + final minute = time.minute; + final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period'; + } + + String _getDayName(DateTime date) { + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return days[date.weekday - 1]; + } + + String _getMonthName(int monthNumber) { + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + if (monthNumber < 1 || monthNumber > 12) return 'Unknown'; + return months[monthNumber - 1]; + } + + Widget _buildHistoryListView(HealthTrackersViewModel viewModel) { + List listItems = []; + final unit = _getUnit(); + + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + listItems = _buildBloodSugarListItems(viewModel, unit); + break; + case HealthTrackerTypeEnum.bloodPressure: + listItems = _buildBloodPressureListItems(viewModel); + break; + case HealthTrackerTypeEnum.weightTracker: + listItems = _buildWeightListItems(viewModel, unit); + break; + } + + if (viewModel.isLoading) { + return _buildLoadingShimmer().paddingOnly(top: 16.h); + } + + if (listItems.isEmpty) { + return _buildEmptyStateWidget(); + } + + return ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: listItems.length, + separatorBuilder: (context, index) => SizedBox.shrink(), + itemBuilder: (context, index) => listItems[index], + ); + } + + /// Build list items for Blood Sugar + List _buildBloodSugarListItems(HealthTrackersViewModel viewModel, String unit) { + List listItems = []; + final allResults = []; + + allResults.addAll(viewModel.weekDiabeticPatientResult); + allResults.addAll(viewModel.monthDiabeticPatientResult); + allResults.addAll(viewModel.yearDiabeticPatientResult); + + final seenIds = {}; + final uniqueResults = allResults.where((result) { + final id = '${result.lineItemNo}_${result.dateChart?.millisecondsSinceEpoch ?? 0}'; + if (seenIds.contains(id)) return false; + seenIds.add(id); + return true; + }).toList(); + + uniqueResults.sort((a, b) { + final dateA = a.dateChart ?? DateTime(1900); + final dateB = b.dateChart ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + + for (var result in uniqueResults) { + final resultValue = result.resultValue?.toDouble() ?? 0.0; + final value = result.resultValue?.toString() ?? '0'; + final resultUnit = result.unit ?? unit; + final measuredDesc = result.measuredDesc ?? ''; + final date = result.dateChart; + final dateLabel = date != null ? '${_getDayName(date)} ${date.day} ${_getMonthName(date.month).substring(0, 3)}, ${date.year}' : ''; + final timeLabel = date != null ? _formatTime(date) : 'Unknown'; + final displayLabel = date != null ? '$dateLabel, $timeLabel' : 'Unknown'; + final subTitleText = '$value $resultUnit'; + + listItems.add( + Column( + children: [ + buildHistoryListTile( + title: displayLabel, + subTitle: subTitleText, + value: resultValue, + measureDesc: measuredDesc, + ), + Divider(height: 1, color: AppColors.dividerColor).paddingOnly(bottom: 8.h), + ], + ), + ); + } + return listItems; + } + + /// Build list items for Blood Pressure + List _buildBloodPressureListItems(HealthTrackersViewModel viewModel) { + List listItems = []; + final allResults = []; + + allResults.addAll(viewModel.weekBloodPressureResult); + allResults.addAll(viewModel.monthBloodPressureResult); + allResults.addAll(viewModel.yearBloodPressureResult); + + final seenIds = {}; + final uniqueResults = allResults.where((result) { + final id = '${result.lineItemNo}_${result.bloodPressureDate?.millisecondsSinceEpoch ?? 0}'; + if (seenIds.contains(id)) return false; + seenIds.add(id); + return true; + }).toList(); + + uniqueResults.sort((a, b) { + final dateA = a.bloodPressureDate ?? DateTime(1900); + final dateB = b.bloodPressureDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + + for (var result in uniqueResults) { + final systolic = result.systolicePressure ?? 0; + final diastolic = result.diastolicPressure ?? 0; + final measuredArmDesc = result.measuredArmDesc ?? ''; + final date = result.bloodPressureDate; + final dateLabel = date != null ? '${_getDayName(date)} ${date.day} ${_getMonthName(date.month).substring(0, 3)}, ${date.year}' : ''; + final timeLabel = date != null ? _formatTime(date) : 'Unknown'; + final displayLabel = date != null ? '$dateLabel, $timeLabel' : 'Unknown'; + final subTitleText = '$systolic/$diastolic mmHg'; + + listItems.add( + Column( + children: [ + buildHistoryListTile( + title: displayLabel, + subTitle: subTitleText, + value: systolic.toDouble(), + measureDesc: measuredArmDesc, + ), + Divider(height: 1, color: AppColors.dividerColor).paddingOnly(bottom: 8.h), + ], + ), + ); + } + return listItems; + } + + /// Build list items for Weight + List _buildWeightListItems(HealthTrackersViewModel viewModel, String unit) { + List listItems = []; + final allResults = []; + + allResults.addAll(viewModel.weekWeightMeasurementResult); + allResults.addAll(viewModel.monthWeightMeasurementResult); + allResults.addAll(viewModel.yearWeightMeasurementResult); + + final seenIds = {}; + final uniqueResults = allResults.where((result) { + final id = '${result.lineItemNo}_${result.weightDate?.millisecondsSinceEpoch ?? 0}'; + if (seenIds.contains(id)) return false; + seenIds.add(id); + return true; + }).toList(); + + uniqueResults.sort((a, b) { + final dateA = a.weightDate ?? DateTime(1900); + final dateB = b.weightDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + + for (var result in uniqueResults) { + final weightValue = result.weightMeasured?.toDouble() ?? 0.0; + final weightUnit = result.unit ?? unit; + final date = result.weightDate; + final dateLabel = date != null ? '${_getDayName(date)} ${date.day} ${_getMonthName(date.month).substring(0, 3)}, ${date.year}' : ''; + final timeLabel = date != null ? _formatTime(date) : 'Unknown'; + final displayLabel = date != null ? '$dateLabel, $timeLabel' : 'Unknown'; + final subTitleText = '${weightValue.toInt()} $weightUnit'; + + listItems.add( + Column( + children: [ + buildHistoryListTile( + title: displayLabel, + subTitle: subTitleText, + value: weightValue, + measureDesc: '', + ), + Divider(height: 1, color: AppColors.dividerColor).paddingOnly(bottom: 8.h), + ], + ), + ); + } + return listItems; + } + + Widget _buildLoadingShimmer({bool isForHistory = true}) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(0.w), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: isForHistory ? 60.h : 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + Widget _buildEmptyStateWidget() { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.calendar, + iconColor: AppColors.textColor, + height: 48.w, + width: 48.w, + ), + SizedBox(height: 16.h), + "You do not have any data available.".toText14( + weight: FontWeight.w500, + color: AppColors.textColor, + isCenter: true, + ), + SizedBox(height: 8.h), + _getEmptyStateMessage().toText12( + color: AppColors.greyTextColor, + isCenter: true, + ), + ], + ), + ), + ); + } + + Widget _buildHistoryGraph() { + return Consumer( + builder: (context, viewModel, _) { + final selectedDuration = viewModel.selectedDurationFilter; + List dataPoints = []; + List? secondaryDataPoints; + + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + dataPoints = _buildBloodSugarGraphData(viewModel, selectedDuration); + break; + case HealthTrackerTypeEnum.bloodPressure: + final (systolicData, diastolicData) = _buildBloodPressureGraphData(viewModel, selectedDuration); + dataPoints = systolicData; // Systolic (primary line) + secondaryDataPoints = diastolicData; // Diastolic (secondary line) + break; + case HealthTrackerTypeEnum.weightTracker: + dataPoints = _buildWeightGraphData(viewModel, selectedDuration); + break; + } + + if (dataPoints.isEmpty) { + return _buildEmptyStateWidget(); + } + + if (viewModel.isLoading) { + return Container( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: _buildLoadingShimmer(), + ); + } + + // Calculate max value from both lines for blood pressure + double maxDataValue = dataPoints.isNotEmpty ? dataPoints.map((p) => p.value).reduce((a, b) => a > b ? a : b) : 0.0; + if (secondaryDataPoints != null && secondaryDataPoints.isNotEmpty) { + final secondaryMax = secondaryDataPoints.map((p) => p.value).reduce((a, b) => a > b ? a : b); + if (secondaryMax > maxDataValue) maxDataValue = secondaryMax; + } + + double maxY = maxDataValue > 200 ? (maxDataValue * 1.2) : 250; + double minY = 0; + double horizontalInterval = maxY / 4; + double leftLabelInterval = horizontalInterval; + + // Set colors based on tracker type + Color graphColor = AppColors.successColor; + Color? secondaryGraphColor; + + if (widget.trackerType == HealthTrackerTypeEnum.bloodPressure) { + graphColor = AppColors.errorColor; // Red for Systolic + secondaryGraphColor = AppColors.blueColor; // Blue for Diastolic + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Legend for blood pressure + if (widget.trackerType == HealthTrackerTypeEnum.bloodPressure) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendItem(AppColors.errorColor, "Systolic".needTranslation), + SizedBox(width: 24.w), + _buildLegendItem(AppColors.blueColor, "Diastolic".needTranslation), + ], + ), + SizedBox(height: 12.h), + ], + CustomGraph( + bottomLabelReservedSize: 30, + dataPoints: dataPoints, + secondaryDataPoints: secondaryDataPoints, + makeGraphBasedOnActualValue: false, + leftLabelReservedSize: 50.h, + showGridLines: true, + maxY: maxY, + minY: minY, + showLinePoints: true, + maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0, + horizontalInterval: horizontalInterval, + leftLabelInterval: leftLabelInterval, + showShadow: widget.trackerType != HealthTrackerTypeEnum.bloodPressure, + graphColor: graphColor, + secondaryGraphColor: secondaryGraphColor, + graphShadowColor: graphColor.withValues(alpha: 0.15), + getDrawingHorizontalLine: (value) { + if (value % horizontalInterval == 0 && value > 0) { + return FlLine( + color: AppColors.greyTextColor.withValues(alpha: 0.3), + strokeWidth: 1.5, + dashArray: [8, 4], + ); + } + return FlLine(color: AppColors.transparent, strokeWidth: 0); + }, + leftLabelFormatter: (value) { + final interval = maxY / 4; + final positions = [0.0, interval, interval * 2, interval * 3, maxY]; + for (var position in positions) { + if ((value - position).abs() < 1) { + return '${value.toInt()}'.toText10(weight: FontWeight.w600); + } + } + return SizedBox.shrink(); + }, + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return SizedBox.shrink(); + if ((value - value.round()).abs() > 0.01) return SizedBox.shrink(); + int index = value.round(); + if (index < 0 || index >= data.length) return SizedBox.shrink(); + + if (selectedDuration == 'Week' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10(weight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + if (selectedDuration == 'Month' && index < 6) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10(weight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + if (selectedDuration == 'Year' && index < 12) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText8(fontWeight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + return SizedBox.shrink(); + }, + scrollDirection: selectedDuration == 'Year' ? Axis.horizontal : Axis.vertical, + height: 250.h, + spotColor: graphColor, + ), + ], + ); + }, + ); + } + + /// Build legend item for graph + Widget _buildLegendItem(Color color, String label) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2.r), + ), + ), + SizedBox(width: 6.w), + label.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ); + } + + /// Build graph data for Blood Sugar + List _buildBloodSugarGraphData(HealthTrackersViewModel viewModel, String selectedDuration) { + List dataPoints = []; + final unit = _getUnit(); + + if (selectedDuration == 'Week') { + final weekResults = viewModel.weekDiabeticResultAverage; + if (weekResults.isNotEmpty) { + final sortedResults = List.from(weekResults); + sortedResults.sort((a, b) => (a.dateChart ?? DateTime.now()).compareTo(b.dateChart ?? DateTime.now())); + final last7Days = sortedResults.length > 7 ? sortedResults.sublist(sortedResults.length - 7) : sortedResults; + + for (var result in last7Days) { + final value = result.dailyAverageResult?.toDouble() ?? 0.0; + final date = result.dateChart ?? DateTime.now(); + final label = _getDayName(date).substring(0, 3); + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Month') { + final monthResults = viewModel.monthDiabeticResultAverage; + if (monthResults.isNotEmpty) { + for (int i = 0; i < monthResults.length; i++) { + final weekData = monthResults[i]; + final value = (weekData.weekAverageResult is num) + ? (weekData.weekAverageResult as num).toDouble() + : double.tryParse(weekData.weekAverageResult?.toString() ?? '0') ?? 0.0; + final weekLabel = weekData.weekDesc ?? 'Week ${i + 1}'; + dataPoints.add(DataPoint( + value: value, + label: 'W${i + 1}', + actualValue: value.toStringAsFixed(1), + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Year') { + final yearResults = viewModel.yearDiabeticResultAverage; + if (yearResults.isNotEmpty) { + for (int targetMonth = 1; targetMonth <= 12; targetMonth++) { + final monthData = yearResults.firstWhere((m) => m.monthNumber == targetMonth, + orElse: () => YearDiabeticResultAverage(monthAverageResult: 0.0, monthNumber: targetMonth, monthName: _getMonthName(targetMonth))); + final value = monthData.monthAverageResult?.toDouble() ?? 0.0; + final monthName = monthData.monthName ?? _getMonthName(targetMonth); + final label = monthName.length >= 3 ? monthName.substring(0, 3) : monthName; + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + } + } + } + return dataPoints; + } + + /// Build graph data for Blood Pressure - returns (systolicData, diastolicData) + (List, List) _buildBloodPressureGraphData(HealthTrackersViewModel viewModel, String selectedDuration) { + List systolicDataPoints = []; + List diastolicDataPoints = []; + const unit = 'mmHg'; + + if (selectedDuration == 'Week') { + final weekResults = viewModel.weekBloodPressureResult; + if (weekResults.isNotEmpty) { + final sortedResults = List.from(weekResults); + sortedResults.sort((a, b) => (a.bloodPressureDate ?? DateTime.now()).compareTo(b.bloodPressureDate ?? DateTime.now())); + final last7Days = sortedResults.length > 7 ? sortedResults.sublist(sortedResults.length - 7) : sortedResults; + + for (var result in last7Days) { + final systolic = (result.systolicePressure ?? 0).toDouble(); + final diastolic = (result.diastolicPressure ?? 0).toDouble(); + final date = result.bloodPressureDate ?? DateTime.now(); + final label = _getDayName(date).substring(0, 3); + + systolicDataPoints.add(DataPoint( + value: systolic, + label: label, + actualValue: '${systolic.toInt()}/${diastolic.toInt()}', + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + + diastolicDataPoints.add(DataPoint( + value: diastolic, + label: label, + actualValue: diastolic.toStringAsFixed(0), + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Month') { + final monthResults = viewModel.monthBloodPressureResult; + if (monthResults.isNotEmpty) { + // Group by week and calculate averages + final Map> weekGroups = {}; + for (var result in monthResults) { + final weekDesc = result.weekDesc ?? 'Week 1'; + weekGroups.putIfAbsent(weekDesc, () => []); + weekGroups[weekDesc]!.add(result); + } + + int weekIndex = 0; + for (var entry in weekGroups.entries) { + final weekData = entry.value; + double avgSystolic = 0; + double avgDiastolic = 0; + + for (var result in weekData) { + avgSystolic += (result.systolicePressure ?? 0); + avgDiastolic += (result.diastolicPressure ?? 0); + } + + if (weekData.isNotEmpty) { + avgSystolic = avgSystolic / weekData.length; + avgDiastolic = avgDiastolic / weekData.length; + } + + final weekLabel = entry.key; + systolicDataPoints.add(DataPoint( + value: avgSystolic, + label: 'W${weekIndex + 1}', + actualValue: '${avgSystolic.toInt()}/${avgDiastolic.toInt()}', + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + + diastolicDataPoints.add(DataPoint( + value: avgDiastolic, + label: 'W${weekIndex + 1}', + actualValue: avgDiastolic.toStringAsFixed(0), + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + + weekIndex++; + } + } + } else if (selectedDuration == 'Year') { + final yearResults = viewModel.yearBloodPressureResult; + if (yearResults.isNotEmpty) { + // Group by month and calculate averages + final Map> monthGroups = {}; + for (var result in yearResults) { + final chartMonth = result.chartMonth; + final monthNum = _getMonthNumber(chartMonth ?? 'January'); + monthGroups.putIfAbsent(monthNum, () => []); + monthGroups[monthNum]!.add(result); + } + + for (int targetMonth = 1; targetMonth <= 12; targetMonth++) { + double avgSystolic = 0; + double avgDiastolic = 0; + + if (monthGroups.containsKey(targetMonth) && monthGroups[targetMonth]!.isNotEmpty) { + final monthData = monthGroups[targetMonth]!; + for (var result in monthData) { + avgSystolic += (result.systolicePressure ?? 0); + avgDiastolic += (result.diastolicPressure ?? 0); + } + avgSystolic = avgSystolic / monthData.length; + avgDiastolic = avgDiastolic / monthData.length; + } + + final monthName = _getMonthName(targetMonth); + final label = monthName.length >= 3 ? monthName.substring(0, 3) : monthName; + + systolicDataPoints.add(DataPoint( + value: avgSystolic, + label: label, + actualValue: '${avgSystolic.toInt()}/${avgDiastolic.toInt()}', + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + + diastolicDataPoints.add(DataPoint( + value: avgDiastolic, + label: label, + actualValue: avgDiastolic.toStringAsFixed(0), + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + } + } + } + return (systolicDataPoints, diastolicDataPoints); + } + + /// Helper to get month number from name + int _getMonthNumber(String monthName) { + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + final index = months.indexWhere((m) => m.toLowerCase() == monthName.toLowerCase()); + return index >= 0 ? index + 1 : 1; + } + + /// Build graph data for Weight + List _buildWeightGraphData(HealthTrackersViewModel viewModel, String selectedDuration) { + List dataPoints = []; + final unit = _getUnit(); + + if (selectedDuration == 'Week') { + final weekResults = viewModel.weekWeightMeasurementResultAverage; + if (weekResults.isNotEmpty) { + final sortedResults = List.from(weekResults); + sortedResults.sort((a, b) => (a.weightDate ?? DateTime.now()).compareTo(b.weightDate ?? DateTime.now())); + final last7Days = sortedResults.length > 7 ? sortedResults.sublist(sortedResults.length - 7) : sortedResults; + + for (var result in last7Days) { + final value = result.dailyAverageResult?.toDouble() ?? 0.0; + final date = result.weightDate ?? DateTime.now(); + final label = _getDayName(date).substring(0, 3); + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: date, + displayTime: _getDayName(date), + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Month') { + final monthResults = viewModel.monthWeightMeasurementResultAverage; + if (monthResults.isNotEmpty) { + for (int i = 0; i < monthResults.length; i++) { + final weekData = monthResults[i]; + final value = weekData.weekAverageResult?.toDouble() ?? 0.0; + final weekLabel = weekData.weekDesc ?? 'Week ${i + 1}'; + dataPoints.add(DataPoint( + value: value, + label: 'W${i + 1}', + actualValue: value.toStringAsFixed(1), + time: DateTime.now(), + displayTime: weekLabel, + unitOfMeasurement: unit)); + } + } + } else if (selectedDuration == 'Year') { + final yearResults = viewModel.yearWeightMeasurementResultAverage; + if (yearResults.isNotEmpty) { + for (int targetMonth = 1; targetMonth <= 12; targetMonth++) { + final monthData = yearResults.firstWhere((m) => m.monthNumber == targetMonth, + orElse: () => + YearWeightMeasurementResultAverage(monthAverageResult: 0.0, monthNumber: targetMonth, monthName: _getMonthName(targetMonth))); + final value = monthData.monthAverageResult?.toDouble() ?? 0.0; + final monthName = monthData.monthName ?? _getMonthName(targetMonth); + final label = monthName.length >= 3 ? monthName.substring(0, 3) : monthName; + dataPoints.add(DataPoint( + value: value, + label: label, + actualValue: value.toStringAsFixed(1), + time: DateTime(DateTime.now().year, targetMonth, 1), + displayTime: monthName, + unitOfMeasurement: unit)); + } + } + } + return dataPoints; + } + + void onSendEmailPressed(BuildContext context) async { + _showEmailInputBottomSheet(context); + } + + /// Show email input bottom sheet + void _showEmailInputBottomSheet(BuildContext context) { + final viewModel = context.read(); + final appState = getIt.get(); + final dialogService = getIt.get(); + + // Get user's email from authenticated user + final userEmail = appState.getAuthenticatedUser()?.emailAddress ?? ''; + + // Create email controller and pre-fill if available + final emailController = TextEditingController(text: userEmail); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: "Send Report by Email".needTranslation, + message: "", + child: _buildEmailInputContent( + context: context, + emailController: emailController, + viewModel: viewModel, + dialogService: dialogService, + ), + onOkPressed: () {}, + ); + } + + /// Build email input content + Widget _buildEmailInputContent({ + required BuildContext context, + required TextEditingController emailController, + required HealthTrackersViewModel viewModel, + required DialogService dialogService, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Enter your email address to receive the report".needTranslation.toText14( + color: AppColors.textColor, + weight: FontWeight.w400, + ), + SizedBox(height: 16.h), + + // Email Input Field using TextInputWidget + TextInputWidget( + padding: EdgeInsets.symmetric(horizontal: 8.w), + labelText: "Email Address".needTranslation, + hintText: "Enter email address".needTranslation, + controller: emailController, + keyboardType: TextInputType.emailAddress, + isEnable: true, + isBorderAllowed: true, + isAllowRadius: true, + ), + + SizedBox(height: 24.h), + + // Send Button + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: "Send Report".needTranslation, + onPressed: () { + _sendEmailReport( + context: context, + email: emailController.text.trim(), + viewModel: viewModel, + dialogService: dialogService, + ); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ); + } + + /// Send email report based on tracker type + Future _sendEmailReport({ + required BuildContext context, + required String email, + required HealthTrackersViewModel viewModel, + required DialogService dialogService, + }) async { + // Validate email + if (email.isEmpty) { + dialogService.showErrorBottomSheet( + message: "Please enter your email address".needTranslation, + ); + return; + } + + // Basic email validation + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(email)) { + dialogService.showErrorBottomSheet( + message: "Please enter a valid email address".needTranslation, + ); + return; + } + + // Close the email input bottom sheet + Navigator.of(context).pop(); + + // Call appropriate email function based on tracker type + switch (widget.trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + await viewModel.sendBloodSugarReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + + case HealthTrackerTypeEnum.bloodPressure: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + + await viewModel.sendBloodPressureReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + + case HealthTrackerTypeEnum.weightTracker: + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + await viewModel.sendWeightReportByEmail( + email: email, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + + _showSuccessMessage(context, dialogService); + }, + onFailure: (error) { + LoaderBottomSheet.hideLoader(); + + dialogService.showErrorBottomSheet(message: error); + }, + ); + break; + } + } + + /// Show success message + void _showSuccessMessage(BuildContext context, DialogService dialogService) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: "Report has been sent to your email successfully".needTranslation, + ), + callBackFunc: () {}, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); + } + + Widget _buildPageShimmer() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + SizedBox(height: 16.h), + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 120.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + ), + ), + SizedBox(height: 16.h), + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 300.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + ), + ), + SizedBox(height: 16.h), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + sendEmail: () async => onSendEmailPressed(context), + title: _getPageTitle(), + bottomChild: Consumer( + builder: (context, viewModel, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Add new Record".needTranslation, + onPressed: () { + if (!viewModel.isLoading) { + context.navigateWithName(AppRoutes.addHealthTrackerEntryPage, arguments: widget.trackerType); + } + }, + icon: AppAssets.add_icon, + borderRadius: 12.r, + borderColor: AppColors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + }, + ), + child: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading) { + return _buildPageShimmer(); + } + + if (_hasNoData(viewModel)) { + return _buildEmptyStateWidget(); + } + + return Column( + children: [ + SizedBox(height: 16.h), + TrackerLastValueCard(trackerType: widget.trackerType), + SizedBox(height: 16.h), + _buildHistoryGraphOrList(), + SizedBox(height: 16.h), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/health_trackers/health_trackers_page.dart b/lib/presentation/health_trackers/health_trackers_page.dart new file mode 100644 index 0000000..c55e8b4 --- /dev/null +++ b/lib/presentation/health_trackers/health_trackers_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; + +class HealthTrackersPage extends StatefulWidget { + const HealthTrackersPage({super.key}); + + @override + State createState() => _HealthTrackersPageState(); +} + +Widget buildHealthTrackerCard({ + required String icon, + required String title, + required String description, + required Color iconBgColor, + required VoidCallback onTap, +}) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: iconBgColor, borderRadius: 10.r), + height: 40.w, + width: 40.w, + child: Utils.buildSvgWithAssets( + icon: icon, + fit: BoxFit.none, + height: 22.w, + width: 22.w, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + title.toText16(weight: FontWeight.w600), + description.toText12( + fontWeight: FontWeight.w500, + color: Color(0xFF8F9AA3), + ), + ], + ), + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets( + icon: AppAssets.arrowRight, + width: 24.w, + height: 24.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor, + ), + ], + ).paddingAll(16.w), + ).onPress(onTap); +} + +class _HealthTrackersPageState extends State { + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: "Health Trackers".needTranslation, + child: Column( + children: [ + buildHealthTrackerCard( + iconBgColor: AppColors.primaryRedColor, + icon: AppAssets.bloodSugarOnlyIcon, + title: "Blood Sugar".needTranslation, + description: "Track your glucose levels, understand trends, and get personalized insights for better health.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.bloodSugar, + ); + }, + ), + SizedBox(height: 16.h), + buildHealthTrackerCard( + iconBgColor: AppColors.infoColor, + icon: AppAssets.bloodPressureIcon, + title: "Blood Pressure".needTranslation, + description: "Monitor your blood pressure levels, track systolic and diastolic readings, and maintain a healthy heart.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.bloodPressure, + ); + }, + ), + SizedBox(height: 16.h), + buildHealthTrackerCard( + iconBgColor: AppColors.successColor, + icon: AppAssets.weightIcon, + title: "Weight".needTranslation, + description: "Track your weight progress, set goals, and maintain a healthy body mass for overall wellness.".needTranslation, + onTap: () { + context.navigateWithName( + AppRoutes.healthTrackerDetailPage, + arguments: HealthTrackerTypeEnum.weightTracker, + ); + }, + ), + ], + ).paddingSymmetrical(20.w, 24.h), + ); + } +} diff --git a/lib/presentation/health_trackers/health_trackers_view_model.dart b/lib/presentation/health_trackers/health_trackers_view_model.dart new file mode 100644 index 0000000..ce1bdf2 --- /dev/null +++ b/lib/presentation/health_trackers/health_trackers_view_model.dart @@ -0,0 +1,1116 @@ +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/blood_pressure_result.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/month_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/week_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/year_blood_pressure_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/diabetic_patient_result.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/month_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/week_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/blood_sugar/year_diabetic_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/month_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/week_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/weight_measurement_result.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/models/weight/year_weight_measurement_result_average.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class HealthTrackersViewModel extends ChangeNotifier { + HealthTrackersRepo healthTrackersRepo; + ErrorHandlerService errorHandlerService; + + HealthTrackersViewModel({required this.healthTrackersRepo, required this.errorHandlerService}); + + // ==================== STATE MANAGEMENT ==================== + bool isLoading = false; + String? _errorMessage; + + String? get errorMessage => _errorMessage; + + List get durationFilters => ["Week", "Month", "Year"]; + + String _selectedDuration = "Week"; + bool _isGraphView = true; + + String get selectedDurationFilter => _selectedDuration; + + bool get isGraphView => _isGraphView; + + final List bloodSugarUnit = ['mg/dlt', 'mol/L']; + + String _selectedBloodSugarUnit = 'mg/dlt'; + + String get selectedBloodSugarUnit => _selectedBloodSugarUnit; + + String _selectedBloodSugarMeasureTime = ''; + + String get selectedBloodSugarMeasureTime => _selectedBloodSugarMeasureTime; + + final List bloodSugarMeasureTimeEnList = [ + 'Before Breakfast', + 'After Breakfast', + 'Before Lunch', + 'After Lunch', + 'Before Dinner', + 'After Dinner', + 'Before Sleep', + 'After Sleep', + 'Fasting', + 'Other', + ]; + final List bloodSugarMeasureTimeArList = [ + "قبل الإفطار", + "بعد الإفطار", + "قبل الغداء", + "بعد الغداء", + "قبل العشاء", + "بعد العشاء", + "قبل النوم", + "بعد النوم", + "صائم", + "آخر", + ]; + + // Setters with notification + void setBloodSugarMeasureTime(String duration) async { + _selectedBloodSugarMeasureTime = duration; + notifyListeners(); + } + + // Setters with notification + void setFilterDuration(String duration) async { + _selectedDuration = duration; + notifyListeners(); + } + + // Setters with notification + void setGraphView(bool value) { + _isGraphView = value; + notifyListeners(); + } + + void setBloodSugarUnit(String unit) { + _selectedBloodSugarUnit = unit; + notifyListeners(); + } + + // ==================== WEIGHT FORM FIELDS ==================== + final List weightUnits = ['kg', 'lb']; + String _selectedWeightUnit = 'kg'; + + String get selectedWeightUnit => _selectedWeightUnit; + + void setWeightUnit(String unit) { + _selectedWeightUnit = unit; + notifyListeners(); + } + + // ==================== BLOOD PRESSURE FORM FIELDS ==================== + final List measuredArmList = ['Left Arm', 'Right Arm']; + String _selectedMeasuredArm = ''; + + String get selectedMeasuredArm => _selectedMeasuredArm; + + void setMeasuredArm(String arm) { + _selectedMeasuredArm = arm; + notifyListeners(); + } + + // Text Controllers + TextEditingController weightController = TextEditingController(); + TextEditingController bloodSugarController = TextEditingController(); + TextEditingController systolicController = TextEditingController(); + TextEditingController diastolicController = TextEditingController(); + + // Get current progress list based on selected duration + // dynamic get currentProgressData { + // switch (_selectedDuration) { + // case 'Daily': + // return _todayProgressList; + // case 'Weekly': + // return _weekProgressList; + // case 'Monthly': + // return _monthProgressList; + // default: + // return _todayProgressList; + // } + // } + + // ==================== WEIGHT TRACKING DATA ==================== + final List _monthWeightMeasurementResultAverage = []; + final List _weekWeightMeasurementResultAverage = []; + final List _yearWeightMeasurementResultAverage = []; + + final List _monthWeightMeasurementResult = []; + final List _weekWeightMeasurementResult = []; + final List _yearWeightMeasurementResult = []; + + // Getters for weight data + List get monthWeightMeasurementResultAverage => _monthWeightMeasurementResultAverage; + + List get weekWeightMeasurementResultAverage => _weekWeightMeasurementResultAverage; + + List get yearWeightMeasurementResultAverage => _yearWeightMeasurementResultAverage; + + List get monthWeightMeasurementResult => _monthWeightMeasurementResult; + + List get weekWeightMeasurementResult => _weekWeightMeasurementResult; + + List get yearWeightMeasurementResult => _yearWeightMeasurementResult; + + // ==================== BLOOD PRESSURE TRACKING DATA ==================== + final List _monthBloodPressureResultAverage = []; + final List _weekBloodPressureResultAverage = []; + final List _yearBloodPressureResultAverage = []; + + final List _monthBloodPressureResult = []; + final List _weekBloodPressureResult = []; + final List _yearBloodPressureResult = []; + + // Getters for blood pressure data + List get monthBloodPressureResultAverage => _monthBloodPressureResultAverage; + + List get weekBloodPressureResultAverage => _weekBloodPressureResultAverage; + + List get yearBloodPressureResultAverage => _yearBloodPressureResultAverage; + + List get monthBloodPressureResult => _monthBloodPressureResult; + + List get weekBloodPressureResult => _weekBloodPressureResult; + + List get yearBloodPressureResult => _yearBloodPressureResult; + + // ==================== BLOOD SUGAR (DIABETIC) TRACKING DATA ==================== + final List _monthDiabeticResultAverage = []; + final List _weekDiabeticResultAverage = []; + final List _yearDiabeticResultAverage = []; + + final List _monthDiabeticPatientResult = []; + final List _weekDiabeticPatientResult = []; + final List _yearDiabeticPatientResult = []; + + // Getters for blood sugar data + List get monthDiabeticResultAverage => _monthDiabeticResultAverage; + + List get weekDiabeticResultAverage => _weekDiabeticResultAverage; + + List get yearDiabeticResultAverage => _yearDiabeticResultAverage; + + List get monthDiabeticPatientResult => _monthDiabeticPatientResult; + + List get weekDiabeticPatientResult => _weekDiabeticPatientResult; + + List get yearDiabeticPatientResult => _yearDiabeticPatientResult; + + // ==================== WEIGHT TRACKING METHODS ==================== + + /// Fetch weight averages and results + Future getWeight() async { + isLoading = true; + notifyListeners(); + + try { + // Fetch weight averages + final averageResult = await healthTrackersRepo.getWeightMeasurementResultAverage(); + + averageResult.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthWeightMeasurementResultAverage.clear(); + _weekWeightMeasurementResultAverage.clear(); + _yearWeightMeasurementResultAverage.clear(); + + // Parse month averages + if (data['monthAverageList'] != null) { + for (var item in (data['monthAverageList'] as List)) { + _monthWeightMeasurementResultAverage.add( + MonthWeightMeasurementResultAverage.fromJson(item), + ); + } + } + + // Parse week averages + if (data['weekAverageList'] != null) { + for (var item in (data['weekAverageList'] as List)) { + _weekWeightMeasurementResultAverage.add( + WeekWeightMeasurementResultAverage.fromJson(item), + ); + } + } + + // Parse year averages + if (data['yearAverageList'] != null) { + for (var item in (data['yearAverageList'] as List)) { + _yearWeightMeasurementResultAverage.add( + YearWeightMeasurementResultAverage.fromJson(item), + ); + } + } + } + }, + ); + + // Fetch weight results + final resultsResponse = await healthTrackersRepo.getWeightMeasurementResults(); + + resultsResponse.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthWeightMeasurementResult.clear(); + _weekWeightMeasurementResult.clear(); + _yearWeightMeasurementResult.clear(); + + // Parse week results + if (data['weekResultList'] != null) { + for (var item in (data['weekResultList'] as List)) { + _weekWeightMeasurementResult.add(WeightMeasurementResult.fromJson(item)); + } + } + + // Parse month results + if (data['monthResultList'] != null) { + for (var item in (data['monthResultList'] as List)) { + _monthWeightMeasurementResult.add(WeightMeasurementResult.fromJson(item)); + } + } + + // Parse year results + if (data['yearResultList'] != null) { + for (var item in (data['yearResultList'] as List)) { + _yearWeightMeasurementResult.add(WeightMeasurementResult.fromJson(item)); + } + } + } + }, + ); + } catch (e) { + log('Error in getWeight: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Add new weight result + Future addWeightResult({ + required String weightDate, + required String weightMeasured, + required int weightUnit, + }) async { + try { + final result = await healthTrackersRepo.addWeightMeasurementResult( + weightDate: weightDate, + weightMeasured: weightMeasured, + weightUnit: weightUnit, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful add + await getWeight(); + }, + ); + + return success; + } catch (e) { + log('Error in addWeightResult: $e'); + return false; + } + } + + /// Update existing weight result + Future updateWeightResult({ + required int lineItemNo, + required int weightUnit, + required String weightMeasured, + required String weightDate, + }) async { + try { + final result = await healthTrackersRepo.updateWeightMeasurementResult( + lineItemNo: lineItemNo, + weightUnit: weightUnit, + weightMeasured: weightMeasured, + weightDate: weightDate, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful update + await getWeight(); + }, + ); + + return success; + } catch (e) { + log('Error in updateWeightResult: $e'); + return false; + } + } + + /// Delete weight result + Future deleteWeightResult({ + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.deactivateWeightMeasurementStatus( + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful delete + await getWeight(); + }, + ); + + return success; + } catch (e) { + return false; + } + } + + /// Send weight report by email + Future sendWeightReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendWeightReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendWeightReportByEmail: $e'); + + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + + // ==================== BLOOD PRESSURE TRACKING METHODS ==================== + + /// Fetch blood pressure averages and results + Future getBloodPressure() async { + isLoading = true; + notifyListeners(); + + try { + // Fetch blood pressure averages + final averageResult = await healthTrackersRepo.getBloodPressureResultAverage(); + + averageResult.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + + if (data is Map) { + // Clear existing data + _monthBloodPressureResultAverage.clear(); + _weekBloodPressureResultAverage.clear(); + _yearBloodPressureResultAverage.clear(); + + // Parse month averages + if (data['monthList'] != null) { + for (var item in (data['monthList'] as List)) { + _monthBloodPressureResultAverage.add( + MonthBloodPressureResultAverage.fromJson(item), + ); + } + } + + // Parse week averages + if (data['weekList'] != null) { + for (var item in (data['weekList'] as List)) { + _weekBloodPressureResultAverage.add( + WeekBloodPressureResultAverage.fromJson(item), + ); + } + } + + // Parse year averages + if (data['yearList'] != null) { + for (var item in (data['yearList'] as List)) { + _yearBloodPressureResultAverage.add( + YearBloodPressureResultAverage.fromJson(item), + ); + } + } + } + }, + ); + + // Fetch blood pressure results + final resultsResponse = await healthTrackersRepo.getBloodPressureResults(); + + resultsResponse.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthBloodPressureResult.clear(); + _weekBloodPressureResult.clear(); + _yearBloodPressureResult.clear(); + + // Parse week results + if (data['weekList'] != null) { + for (var item in (data['weekList'] as List)) { + _weekBloodPressureResult.add(BloodPressureResult.fromJson(item)); + } + } + + // Parse month results + if (data['monthList'] != null) { + for (var item in (data['monthList'] as List)) { + _monthBloodPressureResult.add(BloodPressureResult.fromJson(item)); + } + } + + // Parse year results + if (data['yearList'] != null) { + for (var item in (data['yearList'] as List)) { + _yearBloodPressureResult.add(BloodPressureResult.fromJson(item)); + } + } + } + }, + ); + } catch (e) { + log('Error in getBloodPressure: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Add or Update blood pressure result + Future addOrUpdateBloodPressureResult({ + required String bloodPressureDate, + required String diastolicPressure, + required String systolicePressure, + required int measuredArm, + int? lineItemNo, + bool isUpdate = false, + }) async { + try { + final result = isUpdate + ? await healthTrackersRepo.updateBloodPressureResult( + bloodPressureDate: bloodPressureDate, + diastolicPressure: diastolicPressure, + systolicePressure: systolicePressure, + measuredArm: measuredArm, + lineItemNo: lineItemNo!, + ) + : await healthTrackersRepo.addBloodPressureResult( + bloodPressureDate: bloodPressureDate, + diastolicPressure: diastolicPressure, + systolicePressure: systolicePressure, + measuredArm: measuredArm, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful add/update + await getBloodPressure(); + }, + ); + + return success; + } catch (e) { + log('Error in addOrUpdateBloodPressureResult: $e'); + return false; + } + } + + /// Delete blood pressure result + Future deleteBloodPressureResult({ + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.deactivateBloodPressureStatus( + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful delete + await getBloodPressure(); + }, + ); + + return success; + } catch (e) { + log('Error in deleteBloodPressureResult: $e'); + return false; + } + } + + /// Send blood pressure report by email + Future sendBloodPressureReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendBloodPressureReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendBloodPressureReportByEmail: $e'); + + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + + // ==================== BLOOD SUGAR (DIABETIC) TRACKING METHODS ==================== + + /// Fetch blood sugar averages and results + Future getBloodSugar() async { + isLoading = true; + notifyListeners(); + + try { + // Fetch blood sugar averages + final averageResult = await healthTrackersRepo.getDiabeticResultAverage(); + + averageResult.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthDiabeticResultAverage.clear(); + _weekDiabeticResultAverage.clear(); + _yearDiabeticResultAverage.clear(); + + // Parse month averages + if (data['monthAverageList'] != null) { + for (var item in (data['monthAverageList'] as List)) { + _monthDiabeticResultAverage.add( + MonthDiabeticResultAverage.fromJson(item), + ); + } + } + + // Parse week averages + if (data['weekAverageList'] != null) { + for (var item in (data['weekAverageList'] as List)) { + _weekDiabeticResultAverage.add( + WeekDiabeticResultAverage.fromJson(item), + ); + } + } + + // Parse year averages + if (data['yearAverageList'] != null) { + for (var item in (data['yearAverageList'] as List)) { + _yearDiabeticResultAverage.add( + YearDiabeticResultAverage.fromJson(item), + ); + } + } + } + }, + ); + + // Fetch blood sugar results + final resultsResponse = await healthTrackersRepo.getDiabeticResults(); + + resultsResponse.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) { + final data = apiModel.data; + if (data is Map) { + // Clear existing data + _monthDiabeticPatientResult.clear(); + _weekDiabeticPatientResult.clear(); + _yearDiabeticPatientResult.clear(); + + // Parse week results + if (data['weekResultList'] != null) { + for (var item in (data['weekResultList'] as List)) { + _weekDiabeticPatientResult.add(DiabeticPatientResult.fromJson(item)); + } + } + + // Parse month results + if (data['monthResultList'] != null) { + for (var item in (data['monthResultList'] as List)) { + _monthDiabeticPatientResult.add(DiabeticPatientResult.fromJson(item)); + } + } + + // Parse year results + if (data['yearResultList'] != null) { + for (var item in (data['yearResultList'] as List)) { + _yearDiabeticPatientResult.add(DiabeticPatientResult.fromJson(item)); + } + } + } + }, + ); + } catch (e) { + log('Error in getBloodSugar: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Add new blood sugar result + Future addBloodSugarResult({ + required String bloodSugarDateChart, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + }) async { + try { + final result = await healthTrackersRepo.addDiabeticResult( + bloodSugarDateChart: bloodSugarDateChart, + bloodSugarResult: bloodSugarResult, + diabeticUnit: diabeticUnit, + measuredTime: measuredTime, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful add + await getBloodSugar(); + }, + ); + + return success; + } catch (e) { + log('Error in addBloodSugarResult: $e'); + return false; + } + } + + /// Update existing blood sugar result + Future updateBloodSugarResult({ + required DateTime month, + required DateTime hour, + required String bloodSugarResult, + required String diabeticUnit, + required int measuredTime, + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.updateDiabeticResult( + month: month, + hour: hour, + bloodSugarResult: bloodSugarResult, + diabeticUnit: diabeticUnit, + measuredTime: measuredTime, + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful update + await getBloodSugar(); + }, + ); + + return success; + } catch (e) { + log('Error in updateBloodSugarResult: $e'); + return false; + } + } + + /// Delete blood sugar result + Future deleteBloodSugarResult({ + required int lineItemNo, + }) async { + try { + final result = await healthTrackersRepo.deactivateDiabeticStatus( + lineItemNo: lineItemNo, + ); + + bool success = false; + + result.fold( + (failure) => errorHandlerService.handleError(failure: failure), + (apiModel) async { + success = true; + // Refresh data after successful delete + await getBloodSugar(); + }, + ); + + return success; + } catch (e) { + log('Error in deleteBloodSugarResult: $e'); + return false; + } + } + + /// Send blood sugar report by email + Future sendBloodSugarReportByEmail({ + required String email, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + try { + final result = await healthTrackersRepo.sendBloodSugarReportByEmail( + email: email, + ); + + bool success = false; + + result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + if (onFailure != null) onFailure("Failed to send report by email"); + }, + (apiModel) { + success = true; + if (onSuccess != null) onSuccess(); + }, + ); + + notifyListeners(); + + return success; + } catch (e) { + log('Error in sendBloodSugarReportByEmail: $e'); + if (onFailure != null) onFailure("An error occurred"); + return false; + } + } + + // Validation method + String? _validateBloodSugarEntry(String dateTime) { + // Validate blood sugar value + if (bloodSugarController.text.trim().isEmpty) { + return "Please enter blood sugar value"; + } + + final bloodSugarValue = double.tryParse(bloodSugarController.text.trim()); + if (bloodSugarValue == null) { + return "Please enter a valid number"; + } + + if (bloodSugarValue <= 0) { + return "Blood sugar value must be greater than 0"; + } + + // Validate reasonable range (typical ranges) + if (bloodSugarValue > 1000) { + return "Blood sugar value seems too high. Please check and enter again"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + // Validate measure time + if (_selectedBloodSugarMeasureTime.isEmpty) { + return "Please select when the measurement was taken"; + } + + return null; // No errors + } + + // Save blood sugar entry with validation + Future saveBloodSugarEntry({ + required String dateTime, + required String measureTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateBloodSugarEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get measure time index (0-based, but API expects 1-based) + final measureTimeIndex = bloodSugarMeasureTimeEnList.indexOf(measureTime); + + // Call API + final success = await addBloodSugarResult( + bloodSugarDateChart: dateTime, + bloodSugarResult: bloodSugarController.text.trim(), + diabeticUnit: _selectedBloodSugarUnit, + measuredTime: measureTimeIndex >= 0 ? measureTimeIndex : 0, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + bloodSugarController.clear(); + _selectedBloodSugarMeasureTime = ''; + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save blood sugar entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveBloodSugarEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + // ==================== WEIGHT ENTRY METHODS ==================== + + // Validate weight entry before saving + String? _validateWeightEntry(String dateTime) { + // Validate weight value + final weightValue = weightController.text.trim(); + if (weightValue.isEmpty) { + return "Please enter weight value"; + } + + // Check if it's a valid number + final parsedValue = double.tryParse(weightValue); + if (parsedValue == null || parsedValue <= 0) { + return "Please enter a valid weight value"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + return null; // No errors + } + + // Save weight entry with validation + Future saveWeightEntry({ + required String dateTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateWeightEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get weight unit index (0 = kg, 1 = lb) + final weightUnitIndex = weightUnits.indexOf(_selectedWeightUnit); + + // Call API + final success = await addWeightResult( + weightDate: dateTime, + weightMeasured: weightController.text.trim(), + weightUnit: weightUnitIndex >= 0 ? weightUnitIndex : 0, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + weightController.clear(); + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save weight entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveWeightEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + // ==================== BLOOD PRESSURE ENTRY METHODS ==================== + + // Validate blood pressure entry before saving + String? _validateBloodPressureEntry(String dateTime) { + // Validate systolic value + final systolicValue = systolicController.text.trim(); + if (systolicValue.isEmpty) { + return "Please enter systolic value"; + } + final parsedSystolic = int.tryParse(systolicValue); + if (parsedSystolic == null || parsedSystolic <= 0) { + return "Please enter a valid systolic value"; + } + + // Validate diastolic value + final diastolicValue = diastolicController.text.trim(); + if (diastolicValue.isEmpty) { + return "Please enter diastolic value"; + } + final parsedDiastolic = int.tryParse(diastolicValue); + if (parsedDiastolic == null || parsedDiastolic <= 0) { + return "Please enter a valid diastolic value"; + } + + // Validate arm selection + if (_selectedMeasuredArm.isEmpty) { + return "Please select measured arm"; + } + + // Validate date time + if (dateTime.trim().isEmpty) { + return "Please select date and time"; + } + + return null; // No errors + } + + // Save blood pressure entry with validation + Future saveBloodPressureEntry({ + required String dateTime, + Function()? onSuccess, + Function(String error)? onFailure, + }) async { + // Validate + final validationError = _validateBloodPressureEntry(dateTime); + if (validationError != null) { + _errorMessage = validationError; + if (onFailure != null) onFailure(validationError); + return; + } + + // Clear previous error and show loading + _errorMessage = null; + isLoading = true; + notifyListeners(); + + try { + // Get measured arm index (0 = Left Arm, 1 = Right Arm) + final measuredArmIndex = measuredArmList.indexOf(_selectedMeasuredArm); + + // Call API + final success = await addOrUpdateBloodPressureResult( + bloodPressureDate: dateTime, + systolicePressure: systolicController.text.trim(), + diastolicPressure: diastolicController.text.trim(), + measuredArm: measuredArmIndex >= 0 ? measuredArmIndex : 0, + isUpdate: false, + ); + + isLoading = false; + + if (success) { + // Clear form after successful save + systolicController.clear(); + diastolicController.clear(); + _selectedMeasuredArm = ''; + notifyListeners(); + if (onSuccess != null) onSuccess(); + } else { + _errorMessage = "Failed to save blood pressure entry. Please try again"; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } catch (e) { + log('Error in saveBloodPressureEntry: $e'); + _errorMessage = "An error occurred. Please try again"; + isLoading = false; + notifyListeners(); + if (onFailure != null) onFailure(_errorMessage!); + } + } + + @override + void dispose() { + bloodSugarController.dispose(); + weightController.dispose(); + systolicController.dispose(); + diastolicController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart new file mode 100644 index 0000000..542baa5 --- /dev/null +++ b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart @@ -0,0 +1,271 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class TrackerLastValueCard extends StatelessWidget { + final HealthTrackerTypeEnum trackerType; + + const TrackerLastValueCard({super.key, required this.trackerType}); + + /// Get status text and color based on blood sugar value + (String status, Color color, Color bgColor) _getBloodSugarStatus(double value) { + if (value < 70) { + return ('Low'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.5)); + } else if (value <= 100) { + return ('Normal'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } else if (value <= 125) { + return ('Pre-diabetic'.needTranslation, AppColors.ratingColorYellow, AppColors.errorColor.withValues(alpha: 0.4)); + } else { + return ('High'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.4)); + } + } + + /// Get status text and color based on blood pressure value (systolic) + (String status, Color color, Color bgColor) _getBloodPressureStatus(int systolic) { + if (systolic < 90) { + return ('Low'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.5)); + } else if (systolic <= 120) { + return ('Normal'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } else if (systolic <= 140) { + return ('Elevated'.needTranslation, AppColors.ratingColorYellow, AppColors.errorColor.withValues(alpha: 0.4)); + } else { + return ('High'.needTranslation, AppColors.errorColor, AppColors.errorColor.withValues(alpha: 0.4)); + } + } + + /// Get status for weight (neutral - no good/bad status) + (String status, Color color, Color bgColor) _getWeightStatus() { + return ('Recorded'.needTranslation, AppColors.successColor, AppColors.successLightBgColor); + } + + /// Get default unit based on tracker type + String _getDefaultUnit() { + switch (trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + return 'mg/dL'; + case HealthTrackerTypeEnum.bloodPressure: + return 'mmHg'; + case HealthTrackerTypeEnum.weightTracker: + return 'kg'; + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, viewModel, child) { + // Get the last record based on tracker type + dynamic lastRecord; + String displayValue = '--'; + String unit = _getDefaultUnit(); + DateTime? lastDate; + String status = ''; + Color statusColor = AppColors.greyTextColor; + + switch (trackerType) { + case HealthTrackerTypeEnum.bloodSugar: + final allResults = [ + ...viewModel.weekDiabeticPatientResult, + ...viewModel.monthDiabeticPatientResult, + ...viewModel.yearDiabeticPatientResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.dateChart ?? DateTime(1900); + final dateB = b.dateChart ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final lastValue = lastRecord.resultValue?.toDouble() ?? 0.0; + displayValue = lastValue.toStringAsFixed(0); + unit = lastRecord.unit ?? 'mg/dL'; + lastDate = lastRecord.dateChart; + final (s, c, _) = _getBloodSugarStatus(lastValue); + status = s; + statusColor = c; + } + break; + + case HealthTrackerTypeEnum.bloodPressure: + final allResults = [ + ...viewModel.weekBloodPressureResult, + ...viewModel.monthBloodPressureResult, + ...viewModel.yearBloodPressureResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.bloodPressureDate ?? DateTime(1900); + final dateB = b.bloodPressureDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final systolic = lastRecord.systolicePressure ?? 0; + final diastolic = lastRecord.diastolicPressure ?? 0; + displayValue = '$systolic/$diastolic'; + unit = 'mmHg'; + lastDate = lastRecord.bloodPressureDate; + final (s, c, _) = _getBloodPressureStatus(systolic); + status = s; + statusColor = c; + } + break; + + case HealthTrackerTypeEnum.weightTracker: + final allResults = [ + ...viewModel.weekWeightMeasurementResult, + ...viewModel.monthWeightMeasurementResult, + ...viewModel.yearWeightMeasurementResult, + ]; + if (allResults.isNotEmpty) { + allResults.sort((a, b) { + final dateA = a.weightDate ?? DateTime(1900); + final dateB = b.weightDate ?? DateTime(1900); + return dateB.compareTo(dateA); + }); + lastRecord = allResults.first; + final weightValue = lastRecord.weightMeasured?.toDouble() ?? 0.0; + displayValue = weightValue.toStringAsFixed(0); + unit = lastRecord.unit ?? 'kg'; + lastDate = lastRecord.weightDate; + final (s, c, _) = _getWeightStatus(); + status = s; + statusColor = c; + } + break; + } + + final formattedDate = lastDate != null ? DateFormat('EEE DD MMM, yy').format(lastDate) : DateFormat('EEE DD MMM, yy').format(DateTime.now()); + + // Show shimmer while loading + if (viewModel.isLoading) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40.h, + width: 120.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + SizedBox(height: 8.h), + Row( + children: [ + Container( + height: 32.h, + width: 150.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + ), + SizedBox(width: 8.w), + Container( + height: 32.h, + width: 80.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + ), + ], + ), + ], + ), + ), + ); + } + + // Show empty state if no records + if (lastRecord == null) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "--".toText32(isBold: true, color: AppColors.greyTextColor), + SizedBox(width: 6.w), + unit.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500).paddingOnly(top: 8.h), + ], + ), + SizedBox(height: 8.h), + AppCustomChipWidget( + labelText: "No records yet".needTranslation, + icon: AppAssets.doctor_calendar_icon, + ), + ], + ), + ); + } + + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + displayValue.toText32(isBold: true, color: statusColor), + SizedBox(width: 6.w), + unit.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500).paddingOnly(top: 8.h), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + AppCustomChipWidget( + labelText: "${"Last Record".needTranslation}: $formattedDate", + icon: AppAssets.doctor_calendar_icon, + ), + SizedBox(width: 8.w), + if (trackerType != HealthTrackerTypeEnum.weightTracker) ...[ + AppCustomChipWidget( + labelText: status.needTranslation, + icon: AppAssets.normalStatusGreenIcon, + iconColor: statusColor, + ), + ] + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index a3f286a..4d74bc7 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -1,22 +1,29 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart'; +import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; +import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -38,14 +45,27 @@ class ServicesPage extends StatelessWidget { late final List hmgServices = [ HmgServicesComponentModel( - 11, - "Emergency Services".needTranslation, - "".needTranslation, - AppAssets.emergency_services_icon, - bgColor: AppColors.primaryRedColor, - true, - route: AppRoutes.eReferralPage, - ), + 11, + "Emergency Services".needTranslation, + "".needTranslation, + AppAssets.emergency_services_icon, + bgColor: AppColors.primaryRedColor, + true, + route: null, onTap: () { + getIt.get().flushData(); + getIt.get().getTransportationOrders( + showLoader: false, + ); + getIt.get().getRRTOrders( + showLoader: false, + ); + Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( + CustomPageRoute( + page: EmergencyServicesPage(), + settings: const RouteSettings(name: '/EmergencyServicesPage'), + ), + ); + }), HmgServicesComponentModel( 11, "Book\nAppointment".needTranslation, @@ -53,7 +73,7 @@ class ServicesPage extends StatelessWidget { AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, - route: AppRoutes.eReferralPage, + route: AppRoutes.bookAppointmentPage, ), HmgServicesComponentModel( 5, @@ -64,6 +84,16 @@ class ServicesPage extends StatelessWidget { true, route: AppRoutes.comprehensiveCheckupPage, ), + HmgServicesComponentModel( + 11, + "Indoor Navigation".needTranslation, + "".needTranslation, + AppAssets.indoor_nav_icon, + bgColor: Color(0xff45A2F8), + true, + route: null, + onTap: () {}, + ), HmgServicesComponentModel( 11, "E-Referral Services".needTranslation, @@ -73,30 +103,59 @@ class ServicesPage extends StatelessWidget { true, route: AppRoutes.eReferralPage, ), - HmgServicesComponentModel(3, "Blood Donation".needTranslation, "".needTranslation, AppAssets.blood_donation_icon, bgColor: AppColors.bloodDonationCardColor, true, route: null, onTap: () async { + HmgServicesComponentModel( + 3, + "Blood Donation".needTranslation, + "".needTranslation, + AppAssets.blood_donation_icon, + bgColor: AppColors.bloodDonationCardColor, + true, + route: null, onTap: () async { LoaderBottomSheet.showLoader(loadingText: "Fetching Data..."); await bloodDonationViewModel.getRegionSelectedClinics(onSuccess: (val) async { - await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( - CustomPageRoute( - page: BloodDonationPage(), - ), - ); - }); + // await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( + CustomPageRoute( + page: BloodDonationPage(), + ), + ); + // }, onError: (err) { + // LoaderBottomSheet.hideLoader(); + // }); }, onError: (err) { LoaderBottomSheet.hideLoader(); }); }), - HmgServicesComponentModel( - 3, - "Home Health Care".needTranslation, - "".needTranslation, - AppAssets.homeBottom, - bgColor: AppColors.primaryRedColor, - true, - route: AppRoutes.homeHealthCarePage, - ), + // HmgServicesComponentModel( + // 11, + // "Covid 19 Test".needTranslation, + // "".needTranslation, + // AppAssets.covid19icon, + // bgColor: AppColors.covid29Color, + // true, + // route: AppRoutes.covid19Test, + // ), + + // HmgServicesComponentModel( + // 11, + // "Vital Sign".needTranslation, + // "".needTranslation, + // AppAssets.covid19icon, + // bgColor: AppColors.covid29Color, + // true, + // route: AppRoutes.vitalSign, + // ) + + // HmgServicesComponentModel( + // 3, + // "Home Health Care".needTranslation, + // "".needTranslation, + // AppAssets.homeBottom, + // bgColor: AppColors.primaryRedColor, + // true, + // route: AppRoutes.homeHealthCarePage, + // ), // HmgServicesComponentModel( // 11, // "Virtual Tour".needTranslation, @@ -113,6 +172,75 @@ class ServicesPage extends StatelessWidget { // ) ]; + late final List hmgHealthToolServices = [ + HmgServicesComponentModel( + 11, + "Health Trackers".needTranslation, + "".needTranslation, + AppAssets.general_health, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.healthTrackersPage, + ), + HmgServicesComponentModel( + 11, + "Daily Water Monitor".needTranslation, + "".needTranslation, + AppAssets.daily_water_monitor_icon, + bgColor: AppColors.whiteColor, + true, + route: null, // Set to null since we handle navigation in onTap + onTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Fetching your water intake details.".needTranslation); + final waterMonitorVM = getIt.get(); + final context = getIt.get().navigatorKey.currentContext!; + await waterMonitorVM.fetchUserDetailsForMonitoring( + onSuccess: (userDetail) { + LoaderBottomSheet.hideLoader(); + if (userDetail == null) { + waterMonitorVM.populateFromAuthenticatedUser(); + context.navigateWithName(AppRoutes.waterMonitorSettingsPage); + } else { + context.navigateWithName(AppRoutes.waterConsumptionPage); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + context.navigateWithName(AppRoutes.waterConsumptionPage); + }, + ); + }, + ), + HmgServicesComponentModel( + 11, + "Health\nCalculators".needTranslation, + "".needTranslation, + AppAssets.health_calculators_services_icon, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.healthCalculatorsPage, + ), + HmgServicesComponentModel( + 5, + "Health\nConverters".needTranslation, + "".needTranslation, + AppAssets.health_converters_icon, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.healthConvertersPage, + ), + HmgServicesComponentModel( + 11, + "Smart\nWatches".needTranslation, + "".needTranslation, + AppAssets.smartwatch_icon, + bgColor: AppColors.whiteColor, + true, + route: AppRoutes.smartWatches, + // route: AppRoutes.huaweiHealthExample, + ), + ]; + @override Widget build(BuildContext context) { bloodDonationViewModel = Provider.of(context); @@ -122,308 +250,359 @@ class ServicesPage extends StatelessWidget { body: CollapsingListView( title: "Explore Services".needTranslation, isLeading: false, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - "Medical & Care Services".needTranslation.toText18(isBold: true), - SizedBox(height: 16.h), - GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, // 4 icons per row - crossAxisSpacing: 12.w, - mainAxisSpacing: 18.h, - childAspectRatio: 0.8, - ), - physics: NeverScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + "Medical & Care Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row + crossAxisSpacing: 12.w, + mainAxisSpacing: 18.h, + childAspectRatio: 0.8, + ), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: hmgServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return ServiceGridViewItem(hmgServices[index], index, false, isHealthToolIcon: false); + }, + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + "HMG Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + SizedBox( + height: 350.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getServiceCardsList.length, shrinkWrap: true, - itemCount: hmgServices.length, - padding: EdgeInsets.zero, - itemBuilder: (BuildContext context, int index) { - return ServiceGridViewItem(hmgServices[index], index, false); + padding: EdgeInsets.symmetric(horizontal: 24.w), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: LargeServiceCard( + serviceCardData: LandingPageData.getServiceCardsList[index], + image: LandingPageData.getServiceCardsList[index].icon, + title: LandingPageData.getServiceCardsList[index].title, + subtitle: LandingPageData.getServiceCardsList[index].subtitle, + icon: LandingPageData.getServiceCardsList[index].largeCardIcon, + ), + ), + ), + ); }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), ), - SizedBox(height: 24.h), - "Personal Services".needTranslation.toText18(isBold: true), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: Container( - height: 170.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, + ), + SizedBox(height: 24.h), + "Personal Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + height: 170.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 30.w, height: 30.h), + "Habib Wallet".needTranslation.toText14(weight: FontWeight.w600, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward), + ], + ), + Spacer(), + Consumer(builder: (context, habibWalletVM, child) { + return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, isExpanded: false) + .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); + }), + Spacer(), + CustomButton( + height: 40.h, + icon: AppAssets.recharge_icon, + iconSize: 16.w, + iconColor: AppColors.infoColor, + textColor: AppColors.infoColor, + text: "Recharge".needTranslation, + borderWidth: 0.w, + fontWeight: FontWeight.w500, + borderColor: Colors.transparent, + backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 12.f, + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + }, + ), + ], + ).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + }), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + height: 170.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 30.w, height: 30.h), + "Medical Files".needTranslation.toText14(weight: FontWeight.w600, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward), + ], + ), + Spacer(), + Wrap( + spacing: -8.h, + // runSpacing: 0.h, + children: [ + Utils.buildImgWithAssets( + icon: AppAssets.babyGirlImg, + height: 28.h, + width: 28.w, + border: 1, + fit: BoxFit.contain, + borderRadius: 50.r, + ), + Utils.buildImgWithAssets( + icon: AppAssets.femaleImg, + height: 28.h, + width: 28.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + Utils.buildImgWithAssets( + icon: AppAssets.maleImg, + height: 28.h, + width: 28.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + ], + ), + Spacer(), + CustomButton( + height: 40.h, + icon: AppAssets.add_icon, + iconSize: 16.w, + iconColor: AppColors.primaryRedColor, + textColor: AppColors.primaryRedColor, + text: "Add Member".needTranslation, + borderWidth: 0.w, + fontWeight: FontWeight.w500, + borderColor: Colors.transparent, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 12.f, + onPressed: () { + DialogService dialogService = getIt.get(); + medicalFileViewModel.clearAuthValues(); + dialogService.showAddFamilyFileSheet( + label: "Add Family Member".needTranslation, + message: "Please fill the below field to add a new family member to your profile".needTranslation, + onVerificationPress: () { + medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); + }); + }, + ), + ], + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: MedicalFilePage(), + ), + ); + }), + ), + ), + ], + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + "Health Tools".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row + crossAxisSpacing: 12.w, + mainAxisSpacing: 18.h, + childAspectRatio: 0.8, + ), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: hmgHealthToolServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return ServiceGridViewItem( + hmgHealthToolServices[index], + index, + false, + isHealthToolIcon: true, + ); + }, + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + "Support Services".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 30.w, height: 30.h), - "Habib Wallet".needTranslation.toText14(weight: FontWeight.w600, maxlines: 2).expanded, - Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward), + Utils.buildSvgWithAssets( + icon: AppAssets.virtual_tour_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Virtual Tour".needTranslation.toText12(fontWeight: FontWeight.w500) ], ), - Spacer(), - Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, isExpanded: false) - .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); - }), - Spacer(), - CustomButton( - height: 40.h, - icon: AppAssets.recharge_icon, - iconSize: 16.w, - iconColor: AppColors.infoColor, - textColor: AppColors.infoColor, - text: "Recharge".needTranslation, - borderWidth: 0.w, - fontWeight: FontWeight.w500, - borderColor: Colors.transparent, - backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 12.f, - onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); - }, - ), - ], + ), ).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + Utils.openWebView( + url: 'https://hmgwebservices.com/vt_mobile/html/index.html', + ); }), ), - ), - SizedBox(width: 16.w), - Expanded( - child: Container( - height: 170.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, + SizedBox(width: 16.w), + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 30.w, height: 30.h), - "Medical Files".needTranslation.toText14(weight: FontWeight.w600, maxlines: 2).expanded, - Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward), + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) ], ), - Spacer(), - Wrap( - spacing: -8.h, - // runSpacing: 0.h, + ), + ), + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( children: [ - Utils.buildImgWithAssets( - icon: AppAssets.babyGirlImg, - height: 28.h, - width: 28.w, - border: 1, - fit: BoxFit.contain, - borderRadius: 50.r, - ), - Utils.buildImgWithAssets( - icon: AppAssets.femaleImg, - height: 28.h, - width: 28.w, - border: 1, - borderRadius: 50.r, - fit: BoxFit.contain, - ), - Utils.buildImgWithAssets( - icon: AppAssets.maleImg, - height: 28.h, - width: 28.w, - border: 1, - borderRadius: 50.r, + Utils.buildSvgWithAssets( + icon: AppAssets.latest_news_icon, + width: 32.w, + height: 32.h, fit: BoxFit.contain, ), + SizedBox(width: 8.w), + "Latest News".needTranslation.toText12(fontWeight: FontWeight.w500) ], ), - Spacer(), - CustomButton( - height: 40.h, - icon: AppAssets.add_icon, - iconSize: 16.w, - iconColor: AppColors.primaryRedColor, - textColor: AppColors.primaryRedColor, - text: "Add Member".needTranslation, - borderWidth: 0.w, - fontWeight: FontWeight.w500, - borderColor: Colors.transparent, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 12.f, - onPressed: () { - DialogService dialogService = getIt.get(); - medicalFileViewModel.clearAuthValues(); - dialogService.showAddFamilyFileSheet( - label: "Add Family Member".needTranslation, - message: "Please fill the below field to add a new family member to your profile".needTranslation, - onVerificationPress: () { - medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); - }); - }, - ), - ], + ), ).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: MedicalFilePage(), - ), + Utils.openWebView( + url: 'https://x.com/HMG', ); }), ), - ), - ], - ), - SizedBox(height: 24.h), - "Support Services".needTranslation.toText18(isBold: true), - SizedBox(height: 16.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.virtual_tour_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Virtual Tour".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ), - ), - ).onPress(() { - Utils.openWebView( - url: 'https://hmgwebservices.com/vt_mobile/html/index.html', - ); - }), - ), - SizedBox(width: 16.w), - Expanded( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.car_parking_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ), - ), + SizedBox(width: 16.w), + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, ), - ), - ], - ), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.latest_news_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Latest News".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ), - ), - ).onPress(() { - Utils.openWebView( - url: 'https://x.com/HMG', - ); - }), - ), - SizedBox(width: 16.w), - Expanded( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.hmg_contact_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "HMG Contact".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.hmg_contact_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "HMG Contact".needTranslation.toText12(fontWeight: FontWeight.w500) + ], ), - ).onPress(() { - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.contactUs.tr(), - child: ContactUs(), - callBackFunc: () {}, - isFullScreen: false, - ); - }), - ) - ], - ) - ], - ), - SizedBox(height: 24.h), - ], - ), + ), + ).onPress(() { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.contactUs.tr(), + child: ContactUs(), + callBackFunc: () {}, + isFullScreen: false, + ); + }), + ) + ], + ) + ], + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + ], ), ), ); diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index b24b0b4..3f0c44e 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -13,8 +13,10 @@ class ServiceGridViewItem extends StatelessWidget { final int index; final bool isHomePage; final bool isLocked; + final bool isHealthToolIcon; final Function? onTap; - const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false, this.onTap}); + + const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false, this.onTap, this.isHealthToolIcon = false}); @override Widget build(BuildContext context) { @@ -42,7 +44,7 @@ class ServiceGridViewItem extends StatelessWidget { padding: EdgeInsets.all(12.h), child: Utils.buildSvgWithAssets( icon: hmgServiceComponentModel.icon, - iconColor: AppColors.whiteColor, + iconColor: isHealthToolIcon ? null : AppColors.whiteColor, fit: BoxFit.contain, ), ), diff --git a/lib/presentation/home/data/landing_page_data.dart b/lib/presentation/home/data/landing_page_data.dart index e6716cc..c693479 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -36,7 +35,7 @@ class LandingPageData { isBold: false, ), ServiceCardData( - serviceName: "health_calculators", + serviceName: "health_calculators_and_converts", icon: AppAssets.health_calculators_icon, title: "Health", subtitle: "Calculators", @@ -90,7 +89,7 @@ class LandingPageData { ), ServiceCardData( serviceName: "radiology_results", - icon: AppAssets.home_lab_result_icon, + icon: AppAssets.my_radiology_icon, title: "Radiology", subtitle: "Results", backgroundColor: AppColors.whiteColor, @@ -120,7 +119,7 @@ class LandingPageData { ), ServiceCardData( serviceName: "my_doctors", - icon: AppAssets.insurance_update_icon, + icon: AppAssets.my_doctors_icon, title: "My", subtitle: "Doctors", backgroundColor: AppColors.whiteColor, @@ -130,7 +129,7 @@ class LandingPageData { ), ServiceCardData( serviceName: "sick_leaves", - icon: AppAssets.insurance_update_icon, + icon: AppAssets.my_sick_leave_icon, title: "Sick", subtitle: "Leaves", backgroundColor: AppColors.whiteColor, @@ -142,44 +141,42 @@ class LandingPageData { static List getServiceCardsList = [ ServiceCardData( - icon: AppAssets.liveCareService, + serviceName: "livecare", + icon: AppAssets.small_livecare_icon, title: "LiveCare", subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: Colors.transparent, - iconColor: Colors.transparent, - textColor: Colors.transparent, - isBold: true, - ), - ServiceCardData( - icon: AppAssets.lab_result_icon, - title: "Dermatology", - subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, + largeCardIcon: AppAssets.liveCareService, + backgroundColor: AppColors.successColor, + iconColor: AppColors.whiteColor, isBold: false, ), - ServiceCardData( - icon: AppAssets.my_prescription_icon, + // ServiceCardData( + // icon: AppAssets.homeBottom, + // title: "Dermatology", + // subtitle: "Explore our app, View our services and offers", + // largeCardIcon: AppAssets.homeBottom, + // backgroundColor: AppColors.primaryRedColor, + // isBold: false, + // ), + ServiceCardData( + serviceName: "home_health_care", + icon: AppAssets.homeBottom, title: "Home Health Care", subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, + largeCardIcon: AppAssets.homeHealthCareService, + backgroundColor: AppColors.primaryRedColor, + iconColor: AppColors.whiteColor, isBold: false, ), ServiceCardData( - icon: AppAssets.insurance_update_icon, + serviceName: "pharmacy", + icon: AppAssets.pharmacy_icon, //359846 title: "Pharmacy", subtitle: "Explore our app, View our services and offers", - largeCardIcon: AppAssets.livecare_icon, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, + largeCardIcon: AppAssets.pharmacyService, + backgroundColor: AppColors.pharmacyBGColor, + iconColor: null, + isBold: true, ), ]; } diff --git a/lib/presentation/home/data/service_card_data.dart b/lib/presentation/home/data/service_card_data.dart index 49e7e3d..3bc22d7 100644 --- a/lib/presentation/home/data/service_card_data.dart +++ b/lib/presentation/home/data/service_card_data.dart @@ -7,7 +7,7 @@ class ServiceCardData { final String icon; final String title; final String subtitle; - final Color iconColor; + final Color? iconColor; final Color textColor; final Color backgroundColor; final bool isBold; @@ -22,6 +22,6 @@ class ServiceCardData { this.backgroundColor = AppColors.whiteColor, this.iconColor = AppColors.blackColor, this.textColor = AppColors.blackColor, - this.isBold = false, + this.isBold = false }); } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 96ec558..df03d84 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -9,9 +10,11 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; @@ -20,10 +23,13 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_queue_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/presentation/authentication/quick_login.dart'; @@ -39,11 +45,16 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/countdown_timer.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dart'; import 'package:provider/provider.dart'; @@ -62,13 +73,13 @@ class _LandingPageState extends State { late AppState appState; late MyAppointmentsViewModel myAppointmentsViewModel; - late PrescriptionsViewModel prescriptionsViewModel; final CacheService cacheService = GetIt.instance(); - + late AppointmentRatingViewModel appointmentRatingViewModel; late InsuranceViewModel insuranceViewModel; late ImmediateLiveCareViewModel immediateLiveCareViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late EmergencyServicesViewModel emergencyServicesViewModel; + late TodoSectionViewModel todoSectionViewModel; final SwiperController _controller = SwiperController(); @@ -76,10 +87,7 @@ class _LandingPageState extends State { void initState() { authVM = context.read(); habibWalletVM = context.read(); - // myAppointmentsViewModel = context.read(); - // prescriptionsViewModel = context.read(); - // insuranceViewModel = context.read(); - // immediateLiveCareViewModel = context.read(); + appointmentRatingViewModel = context.read(); authVM.savePushTokenToAppState(); if (mounted) { @@ -91,367 +99,42 @@ class _LandingPageState extends State { if (appState.isAuthenticated) { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); - myAppointmentsViewModel.getPatientMyDoctors(); - prescriptionsViewModel.initPrescriptionsViewModel(); - insuranceViewModel.initInsuranceProvider(); + todoSectionViewModel.initializeTodoSectionViewModel(); immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); emergencyServicesViewModel.checkPatientERAdvanceBalance(); + myAppointmentsViewModel.getPatientAppointmentQueueDetails(); + if(!appState.isRatedVisible) { + appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { + if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { + appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, + onSuccess: ((response) { + appointmentRatingViewModel.setClinicOrDoctor(false); + appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation); + appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation); + openLastRating(); + appState.setRatedVisible(true); + }), + ); + } + }, + ); + } } }); super.initState(); } - Widget buildOptionsForAuthenticatedUser() { - return Column( - children: [ - SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Appointments & Visits".toText16(isBold: true), - Row( - children: [ - LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), - ], - ), - ], - ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); - }), - SizedBox(height: 16.h), - Consumer( - builder: (context, myAppointmentsVM, child) { - return myAppointmentsVM.isMyAppointmentsLoading - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: true, - isFromHomePage: true, - ), - ).paddingSymmetrical(24.h, 0.h) - : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty - ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first, - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: false, - isFromHomePage: true, - ), - ).paddingSymmetrical(24.h, 0.h) - : Swiper( - itemCount: myAppointmentsVM.isMyAppointmentsLoading - ? 3 - : myAppointmentsVM.patientAppointmentsHistoryList.length < 3 - ? myAppointmentsVM.patientAppointmentsHistoryList.length - : 3, - layout: SwiperLayout.STACK, - loop: true, - itemWidth: MediaQuery.of(context).size.width - 48.h, - indicatorLayout: PageIndicatorLayout.COLOR, - axisDirection: AxisDirection.right, - controller: _controller, - itemHeight: 200.h, - pagination: const SwiperPagination( - alignment: Alignment.bottomCenter, - margin: EdgeInsets.only(top: 210 + 8 + 24), - builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), - ), - itemBuilder: (BuildContext context, int index) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: false, - isFromHomePage: true, - ), - ); - }, - ) - : Container( - width: double.infinity, - decoration: - RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), - child: Padding( - padding: EdgeInsets.all(12.h), - child: Column( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), - SizedBox(height: 12.h), - "You do not have any upcoming appointment. Please book an appointment".needTranslation.toText12(isCenter: true), - SizedBox(height: 12.h), - CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), - onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage())); - }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 14.f, - fontWeight: FontWeight.w500, - padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), - icon: AppAssets.add_icon, - iconColor: AppColors.primaryRedColor, - height: 46.h, - ), - ], - ), - ), - ).paddingSymmetrical(24.h, 0.h); - }, - ), - - // Consumer for LiveCare pending request - Consumer( - builder: (context, immediateLiveCareVM, child) { - return immediateLiveCareVM.patientHasPendingLiveCareRequest - ? Column( - children: [ - SizedBox(height: 12.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: true, - side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), - ), - width: double.infinity, - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AppCustomChipWidget( - labelText: immediateLiveCareViewModel.patientLiveCareHistoryList[0].stringCallStatus, - backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), - textColor: AppColors.alertColor, - ), - Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), - // Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 40.h, height: 40.h, fit: BoxFit.contain), - ], - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "You have a pending LiveCare request".needTranslation.toText12(isBold: true), - Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.blackColor, - width: 20.h, - height: 15.h, - fit: BoxFit.contain, - ), - ], - ), - ], - ), - ), - ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage())); - }), - SizedBox(height: 12.h), - ], - ) - : SizedBox(height: 12.h); - }, - ), - - // Consumer for ER Online Check-In pending request - Consumer( - builder: (context, emergencyServicesVM, child) { - return emergencyServicesVM.patientHasAdvanceERBalance - ? Column( - children: [ - SizedBox(height: 4.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: true, - side: BorderSide(color: AppColors.primaryRedColor, width: 3.h), - ), - width: double.infinity, - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AppCustomChipWidget( - labelText: "ER Online Check-In Request", - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), - textColor: AppColors.primaryRedColor, - ), - Utils.buildSvgWithAssets( - icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), - ], - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "You have ER Online Check-In Request".needTranslation.toText12(isBold: true), - Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.blackColor, - width: 20.h, - height: 15.h, - fit: BoxFit.contain, - ), - ], - ), - ], - ), - ), - ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome())); - // context.read().navigateToEROnlineCheckIn(); - }), - SizedBox(height: 12.h), - ], - ) - : SizedBox(height: 12.h); - }, - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Quick Links".needTranslation.toText16(isBold: true), - Row( - children: [ - "View medical file".needTranslation.toText12(color: AppColors.primaryRedColor), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), - ], - ), - ], - ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: MedicalFilePage())); - }), - SizedBox(height: 16.h), - Container( - height: 121.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - child: Column( - children: [ - Expanded( - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: LandingPageData.getLoggedInServiceCardsList.length, - shrinkWrap: true, - padding: EdgeInsets.only(left: 16.h, right: 16.h), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SmallServiceCard( - icon: LandingPageData.getLoggedInServiceCardsList[index].icon, - title: LandingPageData.getLoggedInServiceCardsList[index].title, - subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor, - textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, - backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, - isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, - serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName, - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 10.width, - ), - ), - ], - ), - ).paddingSymmetrical(24.h, 0.h), - ], - ); - } - - Widget buildOptionsForNotAuthenticatedUser() { - return Container( - height: 127.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - child: Column( - children: [ - Expanded( - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: LandingPageData.getNotLoggedInServiceCardsList.length, - shrinkWrap: true, - padding: EdgeInsets.only(left: 16.h, right: 16.h), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SmallServiceCard( - serviceName: LandingPageData.getNotLoggedInServiceCardsList[index].serviceName, - icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, - title: LandingPageData.getNotLoggedInServiceCardsList[index].title, - subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor, - textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, - backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, - isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 0.width, - ), - ), - ], - ), - ).paddingSymmetrical(24.h, 0.h); - } - @override Widget build(BuildContext context) { bookAppointmentsViewModel = Provider.of(context, listen: false); myAppointmentsViewModel = Provider.of(context, listen: false); - prescriptionsViewModel = Provider.of(context, listen: false); insuranceViewModel = Provider.of(context, listen: false); immediateLiveCareViewModel = Provider.of(context, listen: false); emergencyServicesViewModel = Provider.of(context, listen: false); + todoSectionViewModel = Provider.of(context, listen: false); appState = getIt.get(); return PopScope( canPop: false, @@ -478,10 +161,19 @@ class _LandingPageState extends State { text: LocaleKeys.loginOrRegister.tr(context: context), onPressed: () async { await authVM.onLoginPressed(); + + // Navigator.pushReplacementNamed( + // // context, + // context, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments("test123", "123", "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, @@ -492,8 +184,22 @@ class _LandingPageState extends State { mainAxisSize: MainAxisSize.min, spacing: 12.h, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 18.h, width: 18.h), - Utils.buildSvgWithAssets(icon: AppAssets.search_icon, height: 18.h, width: 18.h).onPress(() {}), + Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 18.h, width: 18.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: MedicalFilePage(), + // page: LoginScreen(), + ), + ); + }), + Utils.buildSvgWithAssets(icon: AppAssets.indoor_nav_icon, height: 18.h, width: 18.h).onPress(() { + // Navigator.of(context).push( + // CustomPageRoute( + // page: MedicalFilePage(), + // // page: LoginScreen(), + // ), + // ); + }), Utils.buildSvgWithAssets(icon: AppAssets.contact_icon, height: 18.h, width: 18.h).onPress(() { showCommonBottomSheetWithoutHeight( context, @@ -507,7 +213,513 @@ class _LandingPageState extends State { ), ], ).paddingSymmetrical(24.h, 0.h), - if (appState.isAuthenticated) buildOptionsForAuthenticatedUser() else buildOptionsForNotAuthenticatedUser(), + !appState.isAuthenticated + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + width: 50.w, + height: 60.h, + icon: AppAssets.symptomCheckerIcon, + fit: BoxFit.contain, + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "How are you feeling today?".needTranslation.toText14(isBold: true), + "Check your symptoms with this scale".needTranslation.toText12(fontWeight: FontWeight.w500), + SizedBox(height: 14.h), + CustomButton( + text: "Check your symptoms".needTranslation, + onPressed: () async { + context.navigateWithName(AppRoutes.userInfoSelection); + }, + backgroundColor: Color(0xFF2B353E), + borderColor: Color(0xFF2B353E), + textColor: AppColors.whiteColor, + fontSize: 14, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 40.h, + ), + ], + ) + ], + ), + ), + ).paddingSymmetrical(24.w, 0.h) + : SizedBox.shrink(), + appState.isAuthenticated + ? Column( + children: [ + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Appointments & Visits".toText16(isBold: true), + Row( + children: [ + LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(24.h, 0.h).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); + }), + SizedBox(height: 16.h), + Consumer3( + builder: (context, myAppointmentsVM, immediateLiveCareVM, todoSectionVM, child) { + return myAppointmentsVM.isMyAppointmentsLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: true, + isFromHomePage: true, + ), + ).paddingSymmetrical(24.h, 0.h) + : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty + ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first, + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: true, + ), + ).paddingSymmetrical(24.h, 0.h) + : Swiper( + itemCount: myAppointmentsVM.isMyAppointmentsLoading + ? 3 + : myAppointmentsVM.patientAppointmentsHistoryList.length < 3 + ? myAppointmentsVM.patientAppointmentsHistoryList.length + : 3, + layout: SwiperLayout.STACK, + loop: true, + itemWidth: MediaQuery.of(context).size.width - 48.h, + indicatorLayout: PageIndicatorLayout.COLOR, + axisDirection: AxisDirection.right, + controller: _controller, + itemHeight: 255.h, + pagination: SwiperPagination( + alignment: Alignment.bottomCenter, + margin: EdgeInsets.only(top: 240.h + 8 + 24), + builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), + ), + itemBuilder: (BuildContext context, int index) { + return (myAppointmentsVM.isPatientHasQueueAppointment && index == 0) + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + side: BorderSide(color: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), width: 2.w), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: myAppointmentsVM.currentQueueStatus == 0 ? "In Queue".needTranslation : "Your Turn".needTranslation, + backgroundColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.20), + textColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + ], + ), + SizedBox(height: 8.h), + "Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), + SizedBox(height: 2.h), + "Thank you for your patience, here is your queue number." + .needTranslation + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + SizedBox(height: 8.h), + myAppointmentsVM.currentPatientQueueDetails.queueNo!.toText28(isBold: true), + SizedBox(height: 6.h), + myAppointmentsVM.patientQueueDetailsList.isNotEmpty ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + "Serving Now: ".needTranslation.toText14(isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + myAppointmentsVM.patientQueueDetailsList.first.queueNo!.toText12(isBold: true), + SizedBox(width: 8.w), + AppCustomChipWidget( + deleteIcon: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppAssets.call_for_vitals : AppAssets.call_for_doctor, + labelText: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? "Call for vital signs".needTranslation : "Call for Doctor".needTranslation, + iconColor: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, + textColor: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, + iconSize: 14.w, + backgroundColor: myAppointmentsVM.patientQueueDetailsList.first.callType == 1 ? AppColors.primaryRedColor.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), + labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), + ), + ], + ), + ], + ) : SizedBox(height: 12.h), + SizedBox(height: 5.h), + CustomButton( + text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), + onPressed: () {}, + backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), + borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), + textColor: Utils.getCardButtonTextColor(myAppointmentsVM.currentQueueStatus), + fontSize: 12.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + ], + ), + ), + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: AppointmentQueuePage(), + ), + ); + }) + : (immediateLiveCareVM.patientHasPendingLiveCareRequest && index == 0) + ? Column( + children: [ + SizedBox(height: 12.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: true, + side: BorderSide(color: AppColors.ratingColorYellow, width: 3.h), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Immediate LiveCare Request".needTranslation.toText16(isBold: true), + SizedBox(height: 10.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + AppCustomChipWidget( + labelText: immediateLiveCareVM.patientLiveCareHistoryList[0].stringCallStatus, + backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), + textColor: AppColors.alertColor, + ), + SizedBox(width: 8.w), + AppCustomChipWidget( + icon: AppAssets.appointment_calendar_icon, + labelText: DateUtil.formatDateToDate( + DateUtil.convertStringToDate(immediateLiveCareVM.patientLiveCareHistoryList[0].arrivalTime), false)), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), + // Lottie.asset(AppAnimations.pending_loading_animation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.cover), + ], + ), + SizedBox(height: 10.h), + "Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), + SizedBox(height: 8.h), + "Your turn is after ${immediateLiveCareVM.patientLiveCareHistoryList[0].patCount} patients.".needTranslation.toText14(isBold: true), + SizedBox(height: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Expected waiting time: ".needTranslation.toText12(isBold: true), + SizedBox(height: 7.h), + ValueListenableBuilder( + valueListenable: immediateLiveCareVM.durationNotifier, + builder: (context, duration, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildTime(duration), + ], + ); + }, + ), + ], + ), + // CustomButton( + // text: "View Details".needTranslation, + // onPressed: () async { + // Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage())); + // }, + // backgroundColor: Color(0xffFEE9EA), + // borderColor: Color(0xffFEE9EA), + // textColor: Color(0xffED1C2B), + // fontSize: 14.f, + // fontWeight: FontWeight.w500, + // borderRadius: 12.r, + // padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + // height: 40.h, + // ), + ], + ), + ), + ).paddingSymmetrical(0.h, 0.h).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ImmediateLiveCarePendingRequestPage())); + }), + SizedBox(height: 12.h), + ], + ) + : (todoSectionVM.patientAncillaryOrdersList.isNotEmpty && index == 1) + ? AncillaryOrderCard( + order: todoSectionVM.patientAncillaryOrdersList.first, + isLoading: false, + isOrdersList: false, + onCheckIn: () { + log("Check-in for order: ${todoSectionVM.patientAncillaryOrdersList.first.orderNo}"); + }, + onViewDetails: () { + Navigator.of(context).push( + CustomPageRoute( + page: AncillaryOrderDetailsList( + appointmentNoVida: todoSectionVM.patientAncillaryOrdersList.first.appointmentNo ?? 0, + orderNo: todoSectionVM.patientAncillaryOrdersList.first.orderNo ?? 0, + projectID: todoSectionVM.patientAncillaryOrdersList.first.projectID ?? 0, + projectName: todoSectionVM.patientAncillaryOrdersList.first.projectName ?? "", + ), + ), + ); + }, + ) + : Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: + myAppointmentsVM.patientAppointmentsHistoryList[immediateLiveCareViewModel.patientHasPendingLiveCareRequest ? --index : index], + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: true, + ), + ); + }, + ) + : Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "You do not have any upcoming appointment. Please book an appointment".needTranslation.toText12(isCenter: true), + SizedBox(height: 12.h), + CustomButton( + text: LocaleKeys.bookAppo.tr(context: context), + onPressed: () { + getIt.get().onTabChanged(0); + Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage())); + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14.f, + fontWeight: FontWeight.w500, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + height: 40.h, + ), + ], + ), + ), + ).paddingSymmetrical(24.h, 0.h); + }, + ), + + // Consumer for ER Online Check-In pending request + Consumer( + builder: (context, emergencyServicesVM, child) { + return emergencyServicesVM.patientHasAdvanceERBalance + ? Column( + children: [ + SizedBox(height: 4.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: true, + side: BorderSide(color: AppColors.primaryRedColor, width: 3.h), + ), + width: double.infinity, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + labelText: "ER Online Check-In Request", + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), + textColor: AppColors.primaryRedColor, + ), + Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "You have ER Online Check-In Request".needTranslation.toText12(isBold: true), + Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 20.h, + height: 15.h, + fit: BoxFit.contain, + ), + ], + ), + ], + ), + ), + ).paddingSymmetrical(24.h, 0.h).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome())); + // context.read().navigateToEROnlineCheckIn(); + }), + SizedBox(height: 12.h), + ], + ) + : SizedBox(height: 12.h); + }, + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Quick Links".needTranslation.toText16(isBold: true), + Row( + children: [ + "View medical file".needTranslation.toText12(color: AppColors.primaryRedColor), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(24.h, 0.h).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: MedicalFilePage())); + }), + SizedBox(height: 16.h), + Container( + height: 121.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getLoggedInServiceCardsList.length, + shrinkWrap: true, + padding: EdgeInsets.only(left: 16.h, right: 16.h), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SmallServiceCard( + icon: LandingPageData.getLoggedInServiceCardsList[index].icon, + title: LandingPageData.getLoggedInServiceCardsList[index].title, + subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, + iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor!, + textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, + backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, + isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, + serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName, + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 10.width, + ), + ), + ], + ), + ).paddingSymmetrical(24.h, 0.h), + ], + ) + : Container( + height: 127.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + Expanded( + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getNotLoggedInServiceCardsList.length, + shrinkWrap: true, + padding: EdgeInsets.only(left: 16.h, right: 16.h), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SmallServiceCard( + serviceName: LandingPageData.getNotLoggedInServiceCardsList[index].serviceName, + icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, + title: LandingPageData.getNotLoggedInServiceCardsList[index].title, + subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, + iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor!, + textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, + backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, + isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 0.width, + ), + ), + ], + ), + ).paddingSymmetrical(24.h, 0.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -522,7 +734,7 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: ServicesPage())); }), ], - ).paddingSymmetrical(24.h, 0.h), + ).paddingSymmetrical(24.w, 0.h), SizedBox( height: 340.h, child: ListView.separated( @@ -538,6 +750,7 @@ class _LandingPageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: LargeServiceCard( + serviceCardData: LandingPageData.getServiceCardsList[index], image: LandingPageData.getServiceCardsList[index].icon, title: LandingPageData.getServiceCardsList[index].title, subtitle: LandingPageData.getServiceCardsList[index].subtitle, @@ -547,7 +760,7 @@ class _LandingPageState extends State { ), ); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.w), + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), ), ), appState.isAuthenticated ? HabibWalletCard() : SizedBox(), @@ -569,15 +782,19 @@ class _LandingPageState extends State { isDone: isDone, onPressed: () { // sharedPref.setBool(HAS_ENABLED_QUICK_LOGIN, true); - authVM.loginWithFingerPrintFace(() { + authVM.loginWithFingerPrintFace(() async { isDone = true; cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: true); setState(() {}); + await Future.delayed(Duration(milliseconds: 2000)).then((value) { + if (mounted) Navigator.pop(context); + }); }); }, ); }, ), + // height: isDone == false ? ResponsiveExtension.screenHeight * 0.5 : ResponsiveExtension.screenHeight * 0.3, isFullScreen: false, callBackFunc: () { isDone = true; @@ -585,4 +802,31 @@ class _LandingPageState extends State { }, ); } + + openLastRating() { + showCommonBottomSheetWithoutHeight( + context, + titleWidget: Selector( + selector: (_, vm) => vm.title, + builder: (context, title, child) { + final displayTitle = title ?? ''; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + displayTitle.toText20(weight: FontWeight.w600), + (context.select((vm) => vm.subTitle) ?? '').toText12(), + ], + ); + }, + ), + isCloseButtonVisible: true, + child: StatefulBuilder( + builder: (context, setState) { + + return RateAppointmentDoctor(); + }, + ), + isFullScreen: false, + ); + } } diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index 7fbcdb3..43cc3b9 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; import 'package:hmg_patient_app_new/presentation/hmg_services/services_page.dart'; import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/widgets/bottom_navigation/bottom_navigation.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class LandingNavigation extends StatefulWidget { const LandingNavigation({super.key}); @@ -32,7 +36,9 @@ class _LandingNavigationState extends State { const LandingPage(), appState.isAuthenticated ? MedicalFilePage() : /* need add feedback page */ FeedbackPage(), SizedBox(), - const ToDoPage(), + // const ToDoPage(), + // appState.isAuthenticated ? UserInfoSelectionScreen() : /* need add news page */ SizedBox(), + SizedBox(), ServicesPage(), ], ), @@ -41,9 +47,24 @@ class _LandingNavigationState extends State { onTap: (index) { setState(() => _currentIndex = index); if (_currentIndex == 2) { + getIt.get().onTabChanged(0); context.navigateWithName(AppRoutes.bookAppointmentPage); return; } + if (_currentIndex == 3) { + if (appState.isAuthenticated) { + Navigator.of(context).push( + CustomPageRoute( + page: UserInfoSelectionScreen(), + ), + ); + } else { + Utils.openWebView( + url: 'https://x.com/HMG', + ); + } + return; + } _pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); }, ), diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 6ea4507..b2649f9 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -9,7 +9,6 @@ import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page. import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class HabibWalletCard extends StatelessWidget { diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index 7a155df..5274d5b 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -1,16 +1,25 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; +import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../core/utils/utils.dart'; import '../../../theme/colors.dart'; class LargeServiceCard extends StatelessWidget { + final ServiceCardData serviceCardData; final String image; final String icon; final String title; @@ -18,6 +27,7 @@ class LargeServiceCard extends StatelessWidget { const LargeServiceCard({ super.key, + required this.serviceCardData, this.image = "", this.icon = "", this.title = "", @@ -27,37 +37,112 @@ class LargeServiceCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: 150.w, - padding: EdgeInsets.symmetric(horizontal: 3.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.transparent, borderRadius: 16.r), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + height: 350.h, + width: 230.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.transparent, borderRadius: 24.r), + child: Stack( children: [ - Image.asset(AppAssets.liveCareService, width: 220.w, fit: BoxFit.contain), - SizedBox(height: 10.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: icon, width: 24.w, height: 24.h), - Flexible(child: title.toText14(color: AppColors.blackColor, isBold: true, textOverflow: TextOverflow.clip, maxlines: 1)), - ], + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Image.asset( + serviceCardData.largeCardIcon, + fit: BoxFit.cover, + ), ), - subtitle.toText11(color: AppColors.blackColor), - SizedBox(height: 10.h), - CustomButton( - text: LocaleKeys.bookNow.tr(context: context), - onPressed: () {}, - backgroundColor: AppColors.borderOnlyColor, - borderColor: AppColors.borderOnlyColor, - textColor: AppColors.whiteColor, - fontSize: 14.f, - fontWeight: FontWeight.bold, - borderRadius: 12.r, - padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), - height: 40.h, + Positioned( + bottom: 0.0, // Positions the child 0 logical pixels from the bottom + left: 0.0, + right: 0.0, + child: Container( + height: 180.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + customBorder: BorderRadius.only( + bottomLeft: Radius.circular(24.r), + bottomRight: Radius.circular(24.r), + ), + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 48.h, + width: 48.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: serviceCardData.backgroundColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(12.h), + child: Utils.buildSvgWithAssets( + icon: serviceCardData.icon, + iconColor: serviceCardData.iconColor, + fit: BoxFit.contain, + ), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + serviceCardData.title.toText14(isBold: true, color: AppColors.textColor), + SizedBox(width: 6.h), + serviceCardData.subtitle.toText14(weight: FontWeight.w500, color: AppColors.textColorLight), + ], + ), + ), + ], + ).paddingSymmetrical(16.w, 20.h), + CustomButton( + text: serviceCardData.isBold ? "Visit Pharmacy Online".needTranslation : LocaleKeys.bookNow.tr(context: context), + onPressed: () { + handleOnTap(); + }, + backgroundColor: serviceCardData.isBold ? AppColors.successLightColor.withValues(alpha: 0.2) : AppColors.bgRedLightColor, + borderColor: serviceCardData.isBold ? AppColors.successLightColor.withValues(alpha: 0.01) : AppColors.bgRedLightColor, + textColor: serviceCardData.isBold ? AppColors.successColor : AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + height: 40.h, + ).paddingSymmetrical(16.w, 0.h), + ], + ), + ), ), ], ), ); } + + void handleOnTap() { + switch (serviceCardData.serviceName) { + case "livecare": + { + getIt.get().onTabChanged(1); + Navigator.of(getIt.get().navigatorKey.currentContext!).push( + CustomPageRoute( + page: BookAppointmentPage(), + ), + ); + } + case "home_health_care": + { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: HhcProceduresPage(), + ), + ); + } + case "pharmacy": + { + Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + } } diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index f54f442..6216880 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -1,20 +1,25 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hospital/hospital_selection_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/patient_sickleaves_list_page.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import '../../../core/utils/utils.dart'; import '../../../theme/colors.dart'; +import '../../emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; import '../../radiology/radiology_orders_page.dart' show RadiologyOrdersPage; class SmallServiceCard extends StatelessWidget { @@ -118,10 +123,50 @@ class SmallServiceCard extends StatelessWidget { ), ); break; + + case "indoor_navigation": + openIndoorNavigationBottomSheet(context); default: // Handle unknown service break; } }); } + + void openIndoorNavigationBottomSheet(BuildContext context) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.selectHospital.tr(), + context, + child: ChangeNotifierProvider( + create: (context) => HospitalSelectionBottomSheetViewModel(getIt()), + child: Consumer( + builder: (_, vm, __) => HospitalBottomSheetBody( + searchText: vm.searchController, + displayList: vm.displayList, + onFacilityClicked: (value) { + vm.setSelectedFacility(value); + vm.getDisplayList(); + }, + onHospitalClicked: (hospital) { + Navigator.pop(context); + vm.openPenguin(hospital); + }, + onHospitalSearch: (value) { + vm.searchHospitals(value ?? ""); + }, + selectedFacility: vm.selectedFacility, + hmcCount: vm.hmcCount, + hmgCount: vm.hmgCount, + ), + ), + ), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () { + context.read().clearSearchText(); + }, + ); + } } diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index f710ab2..8ef0697 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/home_health_care/hhc_order_detail_page.dart b/lib/presentation/home_health_care/hhc_order_detail_page.dart index 21b0def..92c93d1 100644 --- a/lib/presentation/home_health_care/hhc_order_detail_page.dart +++ b/lib/presentation/home_health_care/hhc_order_detail_page.dart @@ -10,10 +10,8 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/get_cmc_all_orders_resp_model.dart'; -import 'package:hmg_patient_app_new/presentation/home_health_care/widgets/hhc_ui_selection_helper.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; @@ -75,7 +73,6 @@ class _HhcOrderDetailPageState extends State { Widget _buildOrderCard(GetCMCAllOrdersResponseModel order, {bool isLoading = false}) { final statusColor = _getStatusColor(order.statusId); - final canCancel = order.statusId == 1 || order.statusId == 2; return AnimatedContainer( duration: Duration(milliseconds: 300), @@ -150,27 +147,27 @@ class _HhcOrderDetailPageState extends State { ], ), - // Cancel Button - if (canCancel || isLoading) ...[ - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: "Cancel Order".needTranslation, - onPressed: isLoading ? () {} : () => HhcUiSelectionHelper.showCancelConfirmationDialog(context: context, order: order), - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 10.r, - height: 44.h, - ).toShimmer2(isShow: isLoading), - ), - ], - ), - ] + // // Cancel Button + // if (canCancel || isLoading) ...[ + // SizedBox(height: 16.h), + // Row( + // children: [ + // Expanded( + // child: CustomButton( + // text: "Cancel Order".needTranslation, + // onPressed: isLoading ? () {} : () => HhcUiSelectionHelper.showCancelConfirmationDialog(context: context, order: order), + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // textColor: AppColors.whiteColor, + // fontSize: 14.f, + // fontWeight: FontWeight.w600, + // borderRadius: 10.r, + // height: 44.h, + // ).toShimmer2(isShow: isLoading), + // ), + // ], + // ), + // ] ], ), ), @@ -219,6 +216,7 @@ class _HhcOrderDetailPageState extends State { } return ListView.separated( + padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: viewModel.hhcOrdersList.length, diff --git a/lib/presentation/home_health_care/hhc_procedures_page.dart b/lib/presentation/home_health_care/hhc_procedures_page.dart index bdc2e45..be97a88 100644 --- a/lib/presentation/home_health_care/hhc_procedures_page.dart +++ b/lib/presentation/home_health_care/hhc_procedures_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; @@ -17,6 +18,7 @@ import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/map/map_utility_screen.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -61,28 +63,9 @@ class _HhcProceduresPageState extends State { } Widget _buildPendingOrderCard(GetCMCAllOrdersResponseModel order) { - int status = order.statusId ?? 0; - String statusDisp = order.statusText ?? ""; - Color statusColor; - - if (status == 1) { - // pending - statusColor = AppColors.statusPendingColor; - } else if (status == 2) { - // processing - statusColor = AppColors.statusProcessingColor; - } else if (status == 3) { - // completed - statusColor = AppColors.statusCompletedColor; - } else { - // cancel / rejected - statusColor = AppColors.statusRejectedColor; - } - final canCancel = order.statusId == 1 || order.statusId == 2; final isArabic = getIt.get().isArabic(); - // Extract services from orderselectedservice List selectedServices = []; if (order.orderselectedservice != null) { if (order.orderselectedservice is List) { @@ -108,16 +91,12 @@ class _HhcProceduresPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8.r), - ), - child: statusDisp.toText12( - color: statusColor, - fontWeight: FontWeight.w600, - ), + Row( + children: [ + "Request ID:".needTranslation.toText14(color: AppColors.textColorLight, weight: FontWeight.w500), + SizedBox(width: 4.w), + "${order.iD ?? '-'}".toText16(isBold: true), + ], ), SizedBox(width: 8.w), if (order.created != null) @@ -127,17 +106,7 @@ class _HhcProceduresPageState extends State { ), ], ), - - SizedBox(height: 16.h), - - // Request ID - Row( - children: [ - "Request ID:".needTranslation.toText14(color: AppColors.textColorLight, weight: FontWeight.w500), - SizedBox(width: 4.w), - "${order.iD ?? '-'}".toText16(isBold: true), - ], - ), + SizedBox(height: 8.h), // Services List if (selectedServices.isNotEmpty) ...[ @@ -212,13 +181,13 @@ class _HhcProceduresPageState extends State { ], ), ); - }).toList(), + }), ], ), ), ], - SizedBox(height: 12.h), + SizedBox(height: 8.h), // Info message Container( @@ -274,99 +243,148 @@ class _HhcProceduresPageState extends State { ); } - Widget _buildServiceSelectionList(List services) { - if (services.isEmpty) { - return Center( - child: Padding( - padding: EdgeInsets.all(24.h), - child: Text( - 'No services available'.needTranslation, - style: TextStyle( - fontSize: 16.h, - color: AppColors.greyTextColor, - ), - ), - ), - ); - } - - return Consumer( - builder: (context, viewModel, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 8.h), - SizedBox(height: 16.h), - Text( - 'Select Services'.needTranslation, - style: TextStyle( - fontSize: 20.h, - fontWeight: FontWeight.w700, - color: AppColors.blackColor, - letterSpacing: -0.8, + _buildServicesListBottomsSheet(List services) { + showCommonBottomSheetWithoutHeight( + hasBottomPadding: false, + padding: EdgeInsets.only(top: 24.h), + context, + title: 'Select Services'.needTranslation, + isCloseButtonVisible: true, + isDismissible: true, + callBackFunc: () {}, + child: services.isEmpty + ? Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Text( + 'No services available'.needTranslation, + style: TextStyle( + fontSize: 16.h, + color: AppColors.greyTextColor, + ), + ), ), - ).paddingOnly(left: 16.w, right: 16.w), - SizedBox(height: 12.h), - ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16.w), - itemCount: services.length, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - final service = services[index]; - final isSelected = viewModel.isHhcServiceSelected(service); - final isArabic = getIt.get().isArabic(); - final serviceName = isArabic ? (service.textN ?? service.text ?? '') : (service.text ?? ''); + ) + : Consumer( + builder: (context, hmgServicesViewModel, child) { + final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); + final hasSelectedServices = pendingOrder == null && hmgServicesViewModel.selectedHhcServices.isNotEmpty; - return AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.r, - hasShadow: true, - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => viewModel.toggleHhcServiceSelection(service), - borderRadius: BorderRadius.circular(16.r), - child: Container( - padding: EdgeInsets.all(16.w), - child: Row( - children: [ - Checkbox( - value: isSelected, - onChanged: (v) => viewModel.toggleHhcServiceSelection(service), - activeColor: AppColors.primaryRedColor, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, + return Stack( + children: [ + // Services List + ConstrainedBox( + constraints: BoxConstraints(maxHeight: SizeUtils.height * 0.7), + child: ListView.separated( + separatorBuilder: (context, index) => Divider(color: AppColors.dividerColor).paddingSymmetrical(16.w, 0), + padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: hasSelectedServices ? 160.h : 8.h), + shrinkWrap: true, + itemCount: services.length, + itemBuilder: (context, index) { + final service = services[index]; + final isSelected = hmgServicesViewModel.isHhcServiceSelected(service); + final isArabic = getIt.get().isArabic(); + final serviceName = isArabic ? (service.textN ?? service.text ?? '') : (service.text ?? ''); + + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: InkWell( + onTap: () => hmgServicesViewModel.toggleHhcServiceSelection(service), + borderRadius: BorderRadius.circular(16.r), + child: Container( + padding: EdgeInsets.all(8.w), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor : Colors.transparent, + borderRadius: BorderRadius.circular(5.r), + border: Border.all( + color: isSelected ? AppColors.primaryRedColor : AppColors.borderGrayColor, + width: 1.w, + ), + ), + child: isSelected ? Icon(Icons.check, size: 18.f, color: AppColors.whiteColor) : null, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + serviceName.toText16( + weight: FontWeight.w500, + color: AppColors.blackColor, + maxlines: 2, + ), + ], + ), + ), + ], + ), + ), ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + }, + ), + ), + + if (hasSelectedServices) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - serviceName.toText16( - weight: FontWeight.w400, - color: AppColors.blackColor, - maxlines: 2, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Selected Services".needTranslation.toText12( + color: AppColors.textColorLight, + fontWeight: FontWeight.w600, + ), + "${hmgServicesViewModel.selectedHhcServices.length} service(s) selected".toText14( + isBold: true, + weight: FontWeight.bold, + ), + ], ), ], ), - ), - ], + SizedBox(height: 16.h), + CustomButton( + borderWidth: 0, + text: "Next".needTranslation, + onPressed: () { + Navigator.pop(context); + _proceedWithSelectedService(); + }, + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + ), + ], + ), ), ), - ), - ), + ], ); }, ), - ], - ); - }, ); } @@ -426,77 +444,64 @@ class _HhcProceduresPageState extends State { @override Widget build(BuildContext context) { - return CollapsingListView( - title: "Home Health Care".needTranslation, - history: () => Navigator.of(context).push(CustomPageRoute(page: HhcOrderDetailPage(), direction: AxisDirection.up)), - bottomChild: Consumer( - builder: (context, hmgServicesViewModel, child) { - if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) return SizedBox.shrink(); - final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); - if (pendingOrder == null && hmgServicesViewModel.selectedHhcServices.isNotEmpty) { - return SafeArea( - top: false, - child: Container( - color: AppColors.whiteColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Selected Services Summary Widget - Container( - margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), - padding: EdgeInsets.all(16.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Selected Services".needTranslation.toText12( - color: AppColors.textColorLight, - fontWeight: FontWeight.w600, - ), - "${hmgServicesViewModel.selectedHhcServices.length} service(s) selected".toText14( - isBold: true, - weight: FontWeight.bold, - ), - ], - ), - ], - ), - ), - // Next Button - Padding( - padding: EdgeInsets.only(left: 16.w, right: 16.w), - child: CustomButton( - borderWidth: 0, - text: "Next".needTranslation, - onPressed: _proceedWithSelectedService, - textColor: AppColors.whiteColor, - borderRadius: 12.r, - borderColor: Colors.transparent, - padding: EdgeInsets.symmetric(vertical: 14.h), - ), - ), - ], + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Home Health Care".needTranslation, + history: () => Navigator.of(context).push(CustomPageRoute(page: HhcOrderDetailPage(), direction: AxisDirection.up)), + bottomChild: Consumer( + builder: (BuildContext context, HmgServicesViewModel hmgServicesViewModel, Widget? child) { + if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) { + return SizedBox.shrink(); + } + final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); + if (pendingOrder == null) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, ), - ), - ); - } - return SizedBox.shrink(); - }, - ), - child: Consumer( - builder: (context, hmgServicesViewModel, child) { - if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) { - return _buildLoadingShimmer(); - } - final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); - if (pendingOrder != null) { - return _buildPendingOrderCard(pendingOrder); - } else { - return _buildServiceSelectionList(hmgServicesViewModel.hhcServicesList); - } - }, + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + borderWidth: 0, + text: "Create new request".needTranslation, + onPressed: () => _buildServicesListBottomsSheet(hmgServicesViewModel.hhcServicesList), + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + } + + return SizedBox.shrink(); + }, + ), + child: Consumer( + builder: (context, hmgServicesViewModel, child) { + if (hmgServicesViewModel.isHhcOrdersLoading || hmgServicesViewModel.isHhcServicesLoading) { + return _buildLoadingShimmer(); + } + final pendingOrder = _getPendingOrder(hmgServicesViewModel.hhcOrdersList); + if (pendingOrder != null) { + return _buildPendingOrderCard(pendingOrder); + } else { + return Column( + children: [ + Center( + child: Utils.getNoDataWidget( + context, + noDataText: "You have no pending requests.".needTranslation, + ), + ), + ], + ); + } + }, + ), ), ); } diff --git a/lib/presentation/home_health_care/hhc_selection_review_page.dart b/lib/presentation/home_health_care/hhc_selection_review_page.dart index 8e9ba90..37410e2 100644 --- a/lib/presentation/home_health_care/hhc_selection_review_page.dart +++ b/lib/presentation/home_health_care/hhc_selection_review_page.dart @@ -15,6 +15,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/map/location_map_widget.dart'; @@ -68,40 +69,26 @@ class _HhcSelectionReviewPageState extends State { Widget _buildSelectedServicesCard(bool isArabic) { return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.r, - ), + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), padding: EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "Selected Services".needTranslation.toText14( weight: FontWeight.w600, - color: AppColors.greyTextColor, + color: AppColors.textColor, letterSpacing: -0.4, ), SizedBox(height: 12.h), - ...widget.selectedServices.map((service) { - final serviceName = isArabic ? (service.textN ?? service.text ?? '') : (service.text ?? ''); - final price = service.priceTotal ?? 0.0; - return Padding( - padding: EdgeInsets.only(bottom: 4.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: serviceName.toText14( - weight: FontWeight.w600, - color: AppColors.blackColor, - letterSpacing: -0.5, - maxlines: 2, - ), - ), - ], - ), - ); - }), + Wrap( + spacing: 8.w, + runSpacing: 12.w, + children: widget.selectedServices.map((service) { + final serviceName = isArabic ? (service.textN ?? service.text ?? '') : (service.text ?? ''); + return AppCustomChipWidget(labelText: serviceName.needTranslation); + }).toList(), + ), ], ), ); @@ -117,8 +104,8 @@ class _HhcSelectionReviewPageState extends State { return SizedBox.shrink(); } - final double lat = mapCapturedLocation.lat ?? 0.0; - final double lng = mapCapturedLocation.lng ?? 0.0; + final double lat = mapCapturedLocation?.lat ?? 0.0; + final double lng = mapCapturedLocation?.lng ?? 0.0; if (lat == 0.0 || lng == 0.0) return SizedBox.shrink(); @@ -132,36 +119,25 @@ class _HhcSelectionReviewPageState extends State { latitude: lat, longitude: lng, address: address, - title: "Service Location".needTranslation, onDirectionsTap: () => _launchDirectionsToLocation(lat, lng, address), ); } Widget _buildBottomButton() { - return SafeArea( - top: false, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), - decoration: BoxDecoration( - color: AppColors.whiteColor, - boxShadow: [ - BoxShadow( - color: Color.fromARGB(13, 0, 0, 0), - blurRadius: 8, - offset: Offset(0, -2), - ), - ], - ), - child: CustomButton( - text: "Confirm".needTranslation, - onPressed: _handleConfirm, - textColor: AppColors.whiteColor, - backgroundColor: AppColors.successColor, - borderRadius: 12.r, - borderColor: Colors.transparent, - borderWidth: 0, - padding: EdgeInsets.symmetric(vertical: 14.h), - ), + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: CustomButton( + borderWidth: 0, + text: "Confirm".needTranslation, + onPressed: () => _handleConfirm(), + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, ), ); } diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 150b15c..415d66f 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -146,7 +146,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "${LocaleKeys.usageStatus.tr(context: context)}: ".toText14(isBold: true), - insuranceApprovalResponseModel.apporvalDetails!.isInvoicedDesc!.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + (insuranceApprovalResponseModel.apporvalDetails!.isInvoicedDesc ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), ], ), ], diff --git a/lib/presentation/insurance/insurance_home_page.dart b/lib/presentation/insurance/insurance_home_page.dart index cdd9a2e..b005e42 100644 --- a/lib/presentation/insurance/insurance_home_page.dart +++ b/lib/presentation/insurance/insurance_home_page.dart @@ -15,12 +15,10 @@ import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_upd import 'package:hmg_patient_app_new/presentation/insurance/widgets/patient_insurance_card.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; -import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; import 'package:provider/provider.dart'; import 'widgets/insurance_history.dart'; @@ -74,7 +72,7 @@ class _InsuranceHomePageState extends State { padding: EdgeInsets.only(top: 24.h), child: PatientInsuranceCard( insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first, - isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))), + isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))).paddingSymmetrical(24.w, 0.h), ) : Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.12), diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index bea2e56..ee31538 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -8,11 +8,8 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; class InsuranceApprovalCard extends StatelessWidget { diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index 3e6ee86..341e234 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -12,7 +12,6 @@ import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; import 'package:provider/provider.dart'; class InsuranceHistory extends StatelessWidget { diff --git a/lib/presentation/insurance/widgets/insurance_update_details_card.dart b/lib/presentation/insurance/widgets/insurance_update_details_card.dart index 753a36c..c3bbcd7 100644 --- a/lib/presentation/insurance/widgets/insurance_update_details_card.dart +++ b/lib/presentation/insurance/widgets/insurance_update_details_card.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -12,7 +11,6 @@ import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; import 'package:provider/provider.dart'; class PatientInsuranceCardUpdateCard extends StatelessWidget { diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 3d774d3..fde5811 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -48,8 +48,9 @@ class PatientInsuranceCard extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true), - "Policy: ${insuranceCardDetailsModel.insurancePolicyNo}".toText12(isBold: true, color: AppColors.lightGrayColor), + SizedBox( + width: MediaQuery.of(context).size.width * 0.45, child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true)), + "Policy: ${insuranceCardDetailsModel.insurancePolicyNo}".needTranslation.toText12(isBold: true, color: AppColors.lightGrayColor), ], ), AppCustomChipWidget( diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart index e610d71..29ded69 100644 --- a/lib/presentation/lab/alphabeticScroll.dart +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -5,7 +5,6 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart' import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/debouncer.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; @@ -121,9 +120,7 @@ class _AlphabetScrollPageState extends State { @override Widget build(BuildContext context) { - return - - SizedBox( + return SizedBox( width: MediaQuery.sizeOf(context).width, child: Row( crossAxisAlignment: CrossAxisAlignment.start, // Add this diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index bd56df6..837f482 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -12,7 +11,6 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; class LabOrderByTest extends StatelessWidget { final VoidCallback onTap; diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index ebcd2e2..90651f1 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingToolbar( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[index]); labProvider.getPatientSpecialResult(model.patientLabOrders[index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) // : ListView.builder( // shrinkWrap: true, // physics: NeverScrollableScrollPhysics(), // padding: EdgeInsets.zero, // itemCount: model.isLabOrdersLoading // ? 5 // : model.uniqueTests.toList().isNotEmpty // ? model.uniqueTests.toList().length // : 1, // itemBuilder: (context, index) { // final isExpanded = expandedIndex == index; // return model.isLabOrdersLoading // ? LabResultItemView( // onTap: () {}, // labOrder: null, // index: index, // isLoading: true, // ) // : model.uniqueTests.toList().isNotEmpty // ? AnimationConfiguration.staggeredList( // position: index, // duration: const Duration(milliseconds: 500), // child: SlideAnimation( // verticalOffset: 100.0, // child: FadeInAnimation( // child: LabOrderByTest( // appState: _appState, // onTap: () { // if (model.uniqueTests.toList()[index].model != null) { // rangeViewModel.flush(); // model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, // (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); // } // }, // tests: model.uniqueTests.toList()[index], // index: index, // isExpanded: isExpanded)), // ), // ) // : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); // }, // ) : (model.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) :AlphabeticScroll( alpahbetsAvailable: model.indexedCharacterForUniqueTest, details: model.uniqueTestsList, labViewModel: model, rangeViewModel: rangeViewModel, appState: _appState, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingToolbar( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: Consumer( builder: (context, model, child) { return SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ CustomButton( text: LocaleKeys.byClinic.tr(context: context), onPressed: () { model.setIsSortByClinic(true); }, backgroundColor: model.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, borderColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), SizedBox(width: 8.h), CustomButton( text: LocaleKeys.byHospital.tr(context: context), onPressed: () { model.setIsSortByClinic(false); }, backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available model.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (model.patientLabOrdersViewList.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.patientLabOrdersViewList.length, itemBuilder: (context, index) { final group = model.patientLabOrdersViewList[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder() .toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Text( model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'), style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...group.map((order) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: ("Order No: ".needTranslation + order.orderNo!), ), AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), ), AppCustomChipWidget( labelText: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), ), ], ), // Row( // children: [ // CustomButton( // text: ("Order No: ".needTranslation + order.orderNo!), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), // SizedBox(height: 8.h), // Row( // children: [ // CustomButton( // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), SizedBox(height: 12.h), Row( children: [ Expanded(flex: 2, child: SizedBox()), // Expanded( // flex: 1, // child: Container( // height: 40.h, // width: 40.w, // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // color: AppColors.textColor, // borderRadius: 12, // ), // child: Padding( // padding: EdgeInsets.all(12.h), // child: Transform.flip( // flipX: _appState.isArabic(), // child: Utils.buildSvgWithAssets( // icon: AppAssets.forward_arrow_icon_small, // iconColor: AppColors.whiteColor, // fit: BoxFit.contain, // ), // ), // ), // ).onPress(() { // model.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }), // ) Expanded( flex:2, child: CustomButton( icon: AppAssets.view_report_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, text: "View Results".needTranslation, onPressed: () { model.currentlySelectedPatientOrder = order; labProvider.getPatientLabResultByHospital(order); labProvider.getPatientSpecialResult(order); Navigator.of(context).push( CustomPageRoute(page: LabResultByClinic(labOrder: order)), ); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ) ], ), SizedBox(height: 12.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), SizedBox(height: 12.h), ], ); }).toList(), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation)) : // By Test or other tabs keep existing behavior (model.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: model.indexedCharacterForUniqueTest, details: model.uniqueTestsList, labViewModel: model, rangeViewModel: rangeViewModel, appState: _appState, ) ], ) ); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_item_view.dart b/lib/presentation/lab/lab_result_item_view.dart index 9269c6d..db80918 100644 --- a/lib/presentation/lab/lab_result_item_view.dart +++ b/lib/presentation/lab/lab_result_item_view.dart @@ -9,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; class LabResultItemView extends StatelessWidget { diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart index a603ecb..ad4a032 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart' show tr, StringTranslateExtension; import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index bc1d6b1..c6841b3 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -2,19 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart' show LabViewModel; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; -import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; -import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:provider/provider.dart'; class LabOrderResultItem extends StatelessWidget { diff --git a/lib/presentation/lab/lab_results/lab_result_list_item.dart b/lib/presentation/lab/lab_results/lab_result_list_item.dart index f90df7b..6ccf750 100644 --- a/lib/presentation/lab/lab_results/lab_result_list_item.dart +++ b/lib/presentation/lab/lab_results/lab_result_list_item.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart' ; -import 'package:flutter/src/widgets/framework.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/presentation/lab/search_lab_report.dart b/lib/presentation/lab/search_lab_report.dart index f1fd389..bf27b91 100644 --- a/lib/presentation/lab/search_lab_report.dart +++ b/lib/presentation/lab/search_lab_report.dart @@ -8,7 +8,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:sizer/sizer.dart'; class SearchLabResultsContent extends StatefulWidget { final List labSuggestionsList; diff --git a/lib/presentation/medical_file/eye_measurement_details_page.dart b/lib/presentation/medical_file/eye_measurement_details_page.dart index e69766a..0662cb1 100644 --- a/lib/presentation/medical_file/eye_measurement_details_page.dart +++ b/lib/presentation/medical_file/eye_measurement_details_page.dart @@ -7,7 +7,6 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/main.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index f8d978e..9c53c04 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -3,30 +3,39 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_config.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/allergies/allergies_list_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; @@ -44,9 +53,14 @@ import 'package:hmg_patient_app_new/presentation/medical_file/widgets/lab_rad_ca import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/patient_sick_leave_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/medical_reports_page.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_report/monthly_report.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -54,7 +68,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; @@ -79,24 +92,53 @@ class _MedicalFilePageState extends State { late MedicalFileViewModel medicalFileViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late LabViewModel labViewModel; + late MyInvoicesViewModel myInvoicesViewModel; + late HmgServicesViewModel hmgServicesViewModel; + late PrescriptionsViewModel prescriptionsViewModel; + late MonthlyReportViewModel monthlyReportViewModel; + + final CacheService cacheService = GetIt.instance(); int currentIndex = 0; + // Used to make the PageView height follow the card's intrinsic height + final GlobalKey _vitalSignMeasureKey = GlobalKey(); + double? _vitalSignMeasuredHeight; + @override void initState() { appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { - labViewModel.initLabProvider(); + myAppointmentsViewModel.getPatientMyDoctors(); + prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); medicalFileViewModel.onTabChanged(0); + // Load vital signs + hmgServicesViewModel.getPatientVitalSign(); } }); super.initState(); } + void _scheduleVitalSignMeasure() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _vitalSignMeasureKey.currentContext; + if (ctx == null) return; + final box = ctx.findRenderObject(); + if (box is RenderBox) { + final h = box.size.height; + if (h > 0 && h != _vitalSignMeasuredHeight) { + setState(() { + _vitalSignMeasuredHeight = h; + }); + } + } + }); + } + @override Widget build(BuildContext context) { labViewModel = Provider.of(context, listen: false); @@ -104,9 +146,14 @@ class _MedicalFilePageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); + myInvoicesViewModel = Provider.of(context, listen: false); + hmgServicesViewModel = Provider.of(context, listen: false); + prescriptionsViewModel = Provider.of(context, listen: false); + monthlyReportViewModel = Provider.of(context, listen: false); NavigationService navigationService = getIt.get(); return CollapsingListView( - title: "Medical File".needTranslation, + // title: "Medical File".needTranslation, + title: LocaleKeys.medicalFile.tr(context: context), trailing: Row( children: [ Wrap( @@ -180,8 +227,11 @@ class _MedicalFilePageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" - .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1), + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" + .toText18(isBold: true, weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 2), + ), SizedBox(height: 4.h), Wrap( direction: Axis.horizontal, @@ -191,20 +241,21 @@ class _MedicalFilePageState extends State { AppCustomChipWidget( icon: AppAssets.file_icon, labelText: "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}", - labelPadding: EdgeInsetsDirectional.only(end: 6.w), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), onChipTap: () { navigationService.pushPage( - page: FamilyMedicalScreen( - profiles: medicalFileViewModel.patientFamilyFiles, - onSelect: (FamilyFileResponseModelLists p1) {}, - )); + page: FamilyMedicalScreen( + profiles: medicalFileViewModel.patientFamilyFiles, + onSelect: (FamilyFileResponseModelLists p1) {}, + ), + ); }, ), AppCustomChipWidget( icon: AppAssets.checkmark_icon, labelText: LocaleKeys.verified.tr(context: context), iconColor: AppColors.successColor, - labelPadding: EdgeInsetsDirectional.only(end: 6.w), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), ), ], ), @@ -226,9 +277,9 @@ class _MedicalFilePageState extends State { ), AppCustomChipWidget( icon: AppAssets.blood_icon, - labelText: "Blood: ${appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup.isEmpty}", + labelText: "Blood: ${appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup}", iconColor: AppColors.primaryRedColor, - labelPadding: EdgeInsetsDirectional.only(end: 4.w), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), padding: EdgeInsets.zero, ), Consumer(builder: (context, insuranceVM, child) { @@ -238,8 +289,9 @@ class _MedicalFilePageState extends State { iconColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, textColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 12.w, - backgroundColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), - labelPadding: EdgeInsetsDirectional.only(end: 8.w), + backgroundColor: + insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), ); }), ], @@ -249,6 +301,109 @@ class _MedicalFilePageState extends State { ), ).paddingSymmetrical(24.w, 0.0), SizedBox(height: 16.h), + + // Vital Signs Section + Consumer(builder: (context, hmgServicesVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), + Row( + children: [ + LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(0.w, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }), + SizedBox(height: 16.h), + // Make this section dynamic-height (no fixed 160.h) + LayoutBuilder( + builder: (context, constraints) { + if (hmgServicesVM.isVitalSignLoading) { + return _buildVitalSignShimmer(); + } + if (hmgServicesVM.vitalSignList.isEmpty) { + return Container( + padding: EdgeInsets.all(16.w), + width: MediaQuery.of(context).size.width, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "No vital signs recorded yet".needTranslation.toText12(isCenter: true), + ], + ), + ); + } + + // The cards define their own height; measure the first rendered page once + _scheduleVitalSignMeasure(); + final double hostHeight = _vitalSignMeasuredHeight ?? (160.h); + + return SizedBox( + height: hostHeight, + child: PageView( + controller: hmgServicesVM.vitalSignPageController, + onPageChanged: (index) { + hmgServicesVM.setVitalSignCurrentPage(index); + _scheduleVitalSignMeasure(); + }, + children: _buildVitalSignPages( + vitalSign: hmgServicesVM.vitalSignList.first, + onTap: () { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }, + measureKey: _vitalSignMeasureKey, + currentPageIndex: hmgServicesVM.vitalSignCurrentPage, + ), + ), + ); + }, + ), + if (!hmgServicesVM.isVitalSignLoading && hmgServicesVM.vitalSignList.isNotEmpty) ...[ + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 2, // 2 pages (BMI+Height on page 1, Weight+BP on page 2) + (index) => Container( + margin: EdgeInsets.symmetric(horizontal: 3.w), + width: hmgServicesVM.vitalSignCurrentPage == index ? 24.w : 8.w, + height: 8.h, + decoration: BoxDecoration( + color: hmgServicesVM.vitalSignCurrentPage == index ? AppColors.primaryRedColor : AppColors.dividerColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ), + ), + ], + ], + ).paddingSymmetrical(24.w, 0.0); + }), + SizedBox(height: 16.h), + TextInputWidget( labelText: LocaleKeys.search.tr(context: context), hintText: "Type any record".needTranslation, @@ -267,7 +422,7 @@ class _MedicalFilePageState extends State { // Using CustomExpandableList CustomExpandableList( expansionMode: ExpansionMode.exactlyOne, - dividerColor: Color(0xFF2B353E1A), + dividerColor: Color(0xff2b353e1a), itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), items: [ ExpandableListItem( @@ -298,9 +453,8 @@ class _MedicalFilePageState extends State { title: "Tracker & Others".toText18(weight: FontWeight.w600), expandedBackgroundColor: Colors.transparent, children: [ - Text("Blood Report"), - SizedBox(height: 8), - Text("X-Ray Report"), + SizedBox(height: 10.h), + getSelectedTabData(3), ], ), ], @@ -442,7 +596,8 @@ class _MedicalFilePageState extends State { ? Container( padding: EdgeInsets.all(12.w), width: MediaQuery.of(context).size.width, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: true), + decoration: + RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false), child: Column( children: [ Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), @@ -452,6 +607,7 @@ class _MedicalFilePageState extends State { CustomButton( text: LocaleKeys.bookAppo.tr(context: context), onPressed: () { + getIt.get().onTabChanged(0); Navigator.of(context).push( CustomPageRoute( page: BookAppointmentPage(), @@ -478,13 +634,13 @@ class _MedicalFilePageState extends State { itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: MedicalFileAppointmentCard( patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], @@ -492,11 +648,44 @@ class _MedicalFilePageState extends State { onRescheduleTap: () { openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); }, - onAskDoctorTap: () {}, - )), - ), - ), - ); + onAskDoctorTap: () async { + LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); + await myAppointmentsViewModel.isDoctorAvailable( + projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID, + doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID, + clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID, + onSuccess: (value) async { + if (value) { + await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.askDoctor.tr(context: context), + child: AskDoctorRequestTypeSelect( + askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, + myAppointmentsViewModel: myAppointmentsViewModel, + patientAppointmentHistoryResponseModel: + myAppointmentsVM.patientAppointmentsHistoryList[index], + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + LoaderBottomSheet.hideLoader(); + print("Doctor is not available"); + } + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); + }, + ), + ), + ), + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), ), @@ -555,67 +744,75 @@ class _MedicalFilePageState extends State { child: Column( children: [ ListView.separated( - itemCount: prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, + itemCount: + prescriptionVM.patientPrescriptionOrders.length <= 2 ? prescriptionVM.patientPrescriptionOrders.length : 2, shrinkWrap: true, padding: EdgeInsets.only(left: 0, right: 8.w), physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: Row( - children: [ - Image.network( - prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ).circle(100.r), - SizedBox(width: 16.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true), - SizedBox(height: 4.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.w, - runSpacing: 4.w, - children: [ - AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate( - DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate), - false, + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: Row( + children: [ + Image.network( + prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).circle(100.r), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + prescriptionVM.patientPrescriptionOrders[index].doctorName!.toText16(isBold: true), + SizedBox(height: 4.h), + Wrap( + direction: Axis.horizontal, + spacing: 3.w, + runSpacing: 4.w, + children: [ + AppCustomChipWidget( + labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!), + AppCustomChipWidget( + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate( + DateUtil.convertStringToDate( + prescriptionVM.patientPrescriptionOrders[index].appointmentDate), + false, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - // SizedBox(width: 40.h), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), - ], - ).onPress(() { - prescriptionVM.setPrescriptionsDetailsLoading(); - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionDetailPage(isFromAppointments: false, prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), - ), - ); - }), - ), - ), - ); + // SizedBox(width: 40.h), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + width: 15.w, + height: 15.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor)), + ], + ).onPress(() { + prescriptionVM.setPrescriptionsDetailsLoading(); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDetailPage( + isFromAppointments: false, + prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]), + ), + ); + }), + ), + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), @@ -719,7 +916,10 @@ class _MedicalFilePageState extends State { fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), + ("Dr. John Smith Smith Smith") + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: true), ], ) : myAppointmentsVM.patientMyDoctorsList.isEmpty @@ -746,59 +946,58 @@ class _MedicalFilePageState extends State { shrinkWrap: true, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SizedBox( - // width: 80.w, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, - width: 64.w, - height: 64.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: false, radius: 50.r), - SizedBox(height: 8.h), - Expanded( - child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: false), - ), - ], - ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, - projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, - doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, - )); - LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), - ), - ), - ); + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + // width: 80.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + Expanded( + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, + projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, + doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), @@ -810,7 +1009,7 @@ class _MedicalFilePageState extends State { GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 16.h, + crossAxisSpacing: 10.h, mainAxisSpacing: 16.w, mainAxisExtent: 115.h, ), @@ -907,9 +1106,14 @@ class _MedicalFilePageState extends State { text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}", onPressed: () { insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true); - insuranceViewModel.getPatientInsuranceDetailsForUpdate( - appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + insuranceViewModel.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(), + appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, + child: PatientInsuranceCardUpdateCard(), + callBackFunc: () {}, + title: "", + isCloseButtonVisible: false, + isFullScreen: false); }, backgroundColor: AppColors.bgGreenColor.withOpacity(0.20), borderColor: AppColors.bgGreenColor.withOpacity(0.0), @@ -927,7 +1131,7 @@ class _MedicalFilePageState extends State { GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 16.h, + crossAxisSpacing: 10.h, mainAxisSpacing: 16.w, mainAxisExtent: 120.h, ), @@ -939,7 +1143,7 @@ class _MedicalFilePageState extends State { label: "Update Insurance".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.update_insurance_icon, isLargeText: true, iconSize: 36.w, ).onPress(() { @@ -949,7 +1153,7 @@ class _MedicalFilePageState extends State { label: "${LocaleKeys.insurance.tr(context: context)} ${LocaleKeys.approvals.tr(context: context)}", textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.insurance_approval_icon, isLargeText: true, iconSize: 36.w, ).onPress(() { @@ -963,18 +1167,30 @@ class _MedicalFilePageState extends State { label: "My Invoices List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.invoices_list_icon, isLargeText: true, iconSize: 36.w, - ), + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: MyInvoicesList(), + ), + ); + }), MedicalFileCard( label: "Ancillary Orders List".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.ancillary_orders_list_icon, isLargeText: true, iconSize: 36.w, - ), + ).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: ToDoPage(), + ), + ); + }), ], ).paddingSymmetrical(0.w, 0.0), SizedBox(height: 16.h), @@ -1014,7 +1230,7 @@ class _MedicalFilePageState extends State { GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, - crossAxisSpacing: 16.h, + crossAxisSpacing: 10.h, mainAxisSpacing: 16.w, mainAxisExtent: 110.h, ), @@ -1026,17 +1242,24 @@ class _MedicalFilePageState extends State { label: LocaleKeys.monthlyReports.tr(context: context), textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, + svgIcon: AppAssets.monthly_reports_icon, isLargeText: true, iconSize: 36.h, - ), + ).onPress(() { + monthlyReportViewModel.setHealthSummaryEnabled(cacheService.getBool(key: CacheConst.isMonthlyReportEnabled) ?? false); + Navigator.of(context).push( + CustomPageRoute( + page: MonthlyReport(), + ), + ); + }), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.allergy_info_icon, + svgIcon: AppAssets.medical_reports_icon, isLargeText: true, - iconSize: 36.h, + iconSize: 36.w, ).onPress(() { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.getPatientMedicalReportList(); @@ -1050,7 +1273,7 @@ class _MedicalFilePageState extends State { label: "Sick Leave Report".needTranslation, textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.vaccine_info_icon, + svgIcon: AppAssets.sick_leave_report_icon, isLargeText: true, iconSize: 36.h, ).onPress(() { @@ -1066,9 +1289,350 @@ class _MedicalFilePageState extends State { ], ); case 3: - return Container(); + // Trackers Tab Data + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + "Health Trackers".needTranslation.toText16(weight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(height: 16.h), + GridView( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10.h, + mainAxisSpacing: 16.w, + mainAxisExtent: 115.h, + ), + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + MedicalFileCard( + label: "Blood Sugar".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.blood_sugar_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.bloodSugar)), + MedicalFileCard( + label: "Blood Pressure".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.lab_result_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.bloodPressure)), + MedicalFileCard( + label: "Weight Tracker".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.weight_tracker_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() => context.navigateWithName(AppRoutes.healthTrackerDetailPage, arguments: HealthTrackerTypeEnum.weightTracker)), + ], + ).paddingSymmetrical(0.w, 0.0), + SizedBox(height: 16.h), + Row( + children: [ + "Others".needTranslation.toText16(weight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(height: 16.h), + GridView( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10.h, + mainAxisSpacing: 16.w, + mainAxisExtent: 115.h, + ), + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + MedicalFileCard( + label: "Ask Your Doctor".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.ask_doctor_medical_file_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + MedicalFileCard( + label: "Internet Pairing".needTranslation, + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.internet_pairing_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() {}), + ], + ).paddingSymmetrical(0.w, 0.0), + SizedBox(height: 24.h), + ], + ); default: return Container(); } } + + // Build shimmer for vital signs + Widget _buildVitalSignShimmer() { + return Row( + children: [ + Expanded(child: _buildSingleShimmerCard()), + SizedBox(width: 12.w), + Expanded(child: _buildSingleShimmerCard()), + ], + ); + } + + Widget _buildSingleShimmerCard() { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Icon shimmer at top + Container( + width: 44.w, + height: 44.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + SizedBox(height: 16.h), + // Label shimmer + Container( + width: 70.w, + height: 12.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 8.h), + // Value shimmer (larger) + Container( + width: 60.w, + height: 32.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 12.h), + // Bottom row with chip and arrow + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 60.w, + height: 20.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + Container( + width: 16.w, + height: 16.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + ), + ).toShimmer(), + ], + ), + ], + ), + ), + ); + } + + // Build pages with 2 cards each + List _buildVitalSignPages({ + required VitalSignResModel vitalSign, + required VoidCallback onTap, + required GlobalKey measureKey, + required int currentPageIndex, + }) { + return [ + // Page 1: BMI + Height + Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: "BMI", + value: vitalSign.bodyMassIndex?.toString() ?? '--', + unit: '', + status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, + onTap: onTap, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.heightVital, + label: "Height", + value: vitalSign.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: onTap, + ), + ), + ], + ), + // Page 2: Weight + Blood Pressure + Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: "Weight", + value: vitalSign.weightKg?.toString() ?? '--', + unit: 'kg', + status: vitalSign.weightKg != null ? "Normal" : null, + onTap: onTap, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: "Blood Pressure", + value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ), + onTap: onTap, + ), + ), + ], + ), + ]; + } + + String _getBMIStatus(dynamic bmi) { + return VitalSignUiModel.bmiStatus(bmi); + } + + String? _getBloodPressureStatus({dynamic systolic, dynamic diastolic}) { + return VitalSignUiModel.bloodPressureStatus(systolic: systolic, diastolic: diastolic); + } + + Widget _buildVitalSignCard({ + required String icon, + required String label, + required String value, + required String unit, + required String? status, + required VoidCallback onTap, + }) { + final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label); + + return GestureDetector( + onTap: onTap, + child: Container( + // Same styling used originally for vitals in MedicalFilePage + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + Expanded( + child: label.toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 14.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + value.toText17( + isBold: true, + color: AppColors.textColor, + ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], + ], + ), + if (status != null) + AppCustomChipWidget( + labelText: status, + backgroundColor: scheme.chipBg, + textColor: scheme.chipFg, + ) + else + const SizedBox.shrink(), + ], + ), + ), + SizedBox(height: 8.h), + Align( + alignment: AlignmentDirectional.centerEnd, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 18.w, + height: 18.h, + iconColor: AppColors.textColorLight, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/presentation/medical_file/patient_sickleaves_list_page.dart b/lib/presentation/medical_file/patient_sickleaves_list_page.dart index a28f518..ef5aaeb 100644 --- a/lib/presentation/medical_file/patient_sickleaves_list_page.dart +++ b/lib/presentation/medical_file/patient_sickleaves_list_page.dart @@ -3,6 +3,10 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -12,6 +16,9 @@ import 'package:hmg_patient_app_new/features/medical_file/models/patient_sicklea import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; import 'widgets/patient_sick_leave_card.dart'; @@ -24,6 +31,7 @@ class PatientSickleavesListPage extends StatefulWidget { } class _PatientSickleavesListPageState extends State { + int? expandedIndex; late MedicalFileViewModel medicalFileViewModel; @override @@ -37,51 +45,257 @@ class _PatientSickleavesListPageState extends State { @override Widget build(BuildContext context) { + AppState appState = getIt.get(); medicalFileViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: "${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}", child: SingleChildScrollView( - child: Consumer(builder: (context, medicalFileVM, child) { + child: Consumer(builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListView.separated( - scrollDirection: Axis.vertical, - itemCount: medicalFileVM.isPatientSickLeaveListLoading - ? 3 - : medicalFileVM.patientSickLeaveList.isNotEmpty - ? medicalFileVM.patientSickLeaveList.length + SizedBox(height: 16.h), + // Clinic & Hospital Sort + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + model.setIsSickLeavesSortByClinic(true); + }, + backgroundColor: model.isSickLeavesSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: model.isSickLeavesSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: model.isSickLeavesSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + model.setIsSickLeavesSortByClinic(false); + }, + backgroundColor: model.isSickLeavesSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: model.isSickLeavesSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: model.isSickLeavesSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 20.h), + // Expandable list + ListView.builder( + itemCount: model.isPatientSickLeaveListLoading + ? 4 + : model.patientSickLeaveList.isNotEmpty + ? model.patientSickLeavesViewList.length : 1, - shrinkWrap: true, physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), itemBuilder: (context, index) { - return medicalFileVM.isPatientSickLeaveListLoading + final isExpanded = expandedIndex == index; + return model.isPatientSickLeaveListLoading ? PatientSickLeaveCard( patientSickLeavesResponseModel: PatientSickLeavesResponseModel(), isLoading: true, - ).paddingSymmetrical(24.h, 0.0) - : medicalFileVM.patientSickLeaveList.isNotEmpty + ).paddingSymmetrical(0.h, 12.h) + : model.patientSickLeaveList.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, - duration: const Duration(milliseconds: 1000), + duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( - child: PatientSickLeaveCard( - patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, - isLoading: false, - isSickLeaveListPage: true, - ).paddingSymmetrical(24.h, 0.0), + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${model.patientSickLeavesViewList[index].sickLeavesList!.length} ${LocaleKeys.sickSubtitle.tr(context: context)} Available", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + model.patientSickLeavesViewList[index].filterName!.toText16(isBold: true) + ], + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...model.patientSickLeavesViewList[index].sickLeavesList!.map((sickLeave) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + sickLeave.doctorImageURL!, + width: 24.h, + height: 24.h, + fit: BoxFit.fill, + ).circle(100), + SizedBox(width: 8.h), + Expanded(child: sickLeave.doctorName!.toText14(weight: FontWeight.w500)), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + CustomButton( + text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(sickLeave.appointmentDate), false), + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: model.isSickLeavesSortByClinic ? sickLeave.projectName! : sickLeave.clinicName!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: "${sickLeave.sickLeaveDays} Days", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Expanded( + flex: 6, + child: CustomButton( + text: "Download Report".needTranslation, + onPressed: () async { + LoaderBottomSheet.showLoader(); + await medicalFileViewModel.getPatientSickLeavePDF(sickLeave, appState.getAuthenticatedUser()!).then((val) async { + LoaderBottomSheet.hideLoader(); + if (medicalFileViewModel.patientSickLeavePDFBase64.isNotEmpty) { + String path = await Utils.createFileFromString(medicalFileViewModel.patientSickLeavePDFBase64, "pdf"); + try { + OpenFilex.open(path); + } catch (ex) { + debugPrint("Error opening file: $ex"); + } + } + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppAssets.download, + iconColor: AppColors.primaryRedColor, + iconSize: 14.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }), + ], + ), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any sick leaves yet.".needTranslation); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h), - ), - SizedBox(height: 60.h), + ).paddingSymmetrical(24.h, 0.h), ], ); }), diff --git a/lib/presentation/medical_file/widgets/lab_rad_card.dart b/lib/presentation/medical_file/widgets/lab_rad_card.dart index eef73eb..f31c90b 100644 --- a/lib/presentation/medical_file/widgets/lab_rad_card.dart +++ b/lib/presentation/medical_file/widgets/lab_rad_card.dart @@ -28,59 +28,24 @@ class LabRadCard extends StatelessWidget { AppState appState = getIt.get(); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 18.r, hasShadow: false), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Utils.buildSvgWithAssets( - icon: icon, - width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ).toShimmer2(isShow: false, radius: 12.r), - SizedBox(width: 8.w), - Flexible( - child: labelText.toText12(isBold: true, maxLine: 2), - ), - Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 10.w, height: 10.h, fit: BoxFit.contain, iconColor: AppColors.textColor), - ), - ], + Utils.buildSvgWithAssets( + icon: icon, + width: 40.w, + height: 40.h, + fit: BoxFit.cover, + ).toShimmer2(isShow: false, radius: 12.r), + SizedBox(width: 8.w), + Flexible( + child: labelText.toText12(isBold: true, maxLine: 2), + ), + SizedBox(width: 12.w), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 10.w, height: 10.h, fit: BoxFit.contain, iconColor: AppColors.textColor), ), - // SizedBox(height: 16.h), - // labOrderTests.isNotEmpty - // ? ListView.separated( - // scrollDirection: Axis.vertical, - // padding: EdgeInsets.zero, - // physics: NeverScrollableScrollPhysics(), - // shrinkWrap: true, - // itemBuilder: (cxt, index) { - // return labOrderTests[index] - // .toText12(isBold: true, maxLine: 1) - // .toShimmer2(isShow: false, radius: 6.r, height: 24.h, width: 120.w) - // .toShimmer2(isShow: isLoading); - // }, - // separatorBuilder: (cxt, index) => SizedBox(height: 8.h), - // itemCount: 3, - // ) - // : "You don't have any records yet".needTranslation.toText13( - // color: AppColors.greyTextColor, isCenter: true), - // SizedBox(height: 16.h), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // SizedBox.shrink(), - // Transform.flip( - // flipX: appState.isArabic(), - // child: Utils.buildSvgWithAssets( - // icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor) - // .toShimmer2(isShow: false, radius: 12.r), - // ), - // ], - // ) ], ).paddingAll(12.w), ); diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 1b09dd5..fbe79bb 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -173,7 +173,7 @@ class MedicalFileAppointmentCard extends StatelessWidget { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), diff --git a/lib/presentation/medical_file/widgets/medical_file_card.dart b/lib/presentation/medical_file/widgets/medical_file_card.dart index 6343e2a..77968c5 100644 --- a/lib/presentation/medical_file/widgets/medical_file_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_card.dart @@ -28,7 +28,7 @@ class MedicalFileCard extends StatelessWidget { return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: backgroundColor, - borderRadius: 12.r, + borderRadius: 20.r, hasShadow: false ), padding: EdgeInsets.all(12.w), diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index a818ae5..6f9b8b5 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -18,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index 71abcb7..f6d7576 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -2,6 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -10,14 +13,14 @@ import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_mode import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/medical_report_request_page.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/presentation/medical_report/widgets/patient_medical_report_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:provider/provider.dart'; class MedicalReportsPage extends StatefulWidget { @@ -28,10 +31,12 @@ class MedicalReportsPage extends StatefulWidget { } class _MedicalReportsPageState extends State { + int? expandedIndex; late MedicalFileViewModel medicalFileViewModel; @override Widget build(BuildContext context) { + AppState appState = getIt.get(); medicalFileViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, @@ -45,15 +50,53 @@ class _MedicalReportsPageState extends State { return Column( children: [ SizedBox(height: 16.h), + // Status filter tabs + // Row( + // children: [ + // CustomButton( + // text: LocaleKeys.byClinic.tr(context: context), + // onPressed: () { + // medicalFileVM.setIsMedicalReportsSortByClinic(true); + // }, + // backgroundColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + // borderColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + // textColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // borderRadius: 10, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // SizedBox(width: 8.h), + // CustomButton( + // text: LocaleKeys.byHospital.tr(context: context), + // onPressed: () { + // medicalFileVM.setIsMedicalReportsSortByClinic(false); + // }, + // backgroundColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + // borderColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + // textColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // borderRadius: 10, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // ], + // ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 8.h), Row( children: [ CustomButton( text: "Requested".needTranslation, onPressed: () { + setState(() { + expandedIndex = null; + }); medicalFileViewModel.onMedicalReportTabChange(0); }, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, @@ -65,10 +108,13 @@ class _MedicalReportsPageState extends State { CustomButton( text: LocaleKeys.ready.tr(context: context), onPressed: () { + setState(() { + expandedIndex = null; + }); medicalFileViewModel.onMedicalReportTabChange(1); }, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, @@ -80,10 +126,13 @@ class _MedicalReportsPageState extends State { CustomButton( text: LocaleKeys.cancelled.tr(context: context), onPressed: () { + setState(() { + expandedIndex = null; + }); medicalFileViewModel.onMedicalReportTabChange(2); }, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, @@ -93,34 +142,27 @@ class _MedicalReportsPageState extends State { ), ], ).paddingSymmetrical(24.h, 0.h), - // CustomTabBar( - // activeTextColor: Color(0xffED1C2B), - // activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - // tabs: [ - // CustomTabBarModel(null, "Requested".needTranslation), - // CustomTabBarModel(null, "Ready".needTranslation), - // CustomTabBarModel(null, "Cancelled".needTranslation), - // ], - // onTabChange: (index) { - // medicalFileViewModel.onMedicalReportTabChange(index); - // }, - // ).paddingSymmetrical(24.h, 0.h), - ListView.separated( - padding: EdgeInsets.only(top: 24.h), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), + SizedBox(height: 12.h), + // Clinic & Hospital Sort + + // Expandable list + ListView.builder( itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading - ? 3 + ? 4 : medicalFileViewModel.patientMedicalReportList.isNotEmpty - ? medicalFileViewModel.patientMedicalReportList.length + ? medicalFileViewModel.patientMedicalReportsViewList.length : 1, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: const EdgeInsets.only(left: 0, right: 8), itemBuilder: (context, index) { + final isExpanded = expandedIndex == index; return medicalFileViewModel.isPatientMedicalReportsListLoading ? PatientMedicalReportCard( patientMedicalReportResponseModel: PatientMedicalReportResponseModel(), medicalFileViewModel: medicalFileVM, isLoading: true, - ).paddingSymmetrical(24.h, 0.h) + ).paddingSymmetrical(0.h, 8.h) : medicalFileViewModel.patientMedicalReportList.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, @@ -131,20 +173,185 @@ class _MedicalReportsPageState extends State { child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: PatientMedicalReportCard( - patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index], - medicalFileViewModel: medicalFileVM, - isLoading: false, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${medicalFileVM.patientMedicalReportsViewList[index].medicalReportsList!.length} Reports Available", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + medicalFileVM.patientMedicalReportsViewList[index].filterName!.toText16(isBold: true) + ], + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...medicalFileVM.patientMedicalReportsViewList[index].medicalReportsList!.map((report) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + report.doctorImageURL!, + width: 24.h, + height: 24.h, + fit: BoxFit.fill, + ).circle(100), + SizedBox(width: 8.h), + Expanded(child: report.doctorName!.toText14(weight: FontWeight.w500)), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + CustomButton( + text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.appointmentDate), false), + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: medicalFileVM.isMedicalReportsSortByClinic ? report.projectName! : report.clinicDescription!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: report.statusDesc!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + if (medicalFileVM.selectedMedicalReportsTabIndex == 1) + Expanded( + flex: 6, + child: CustomButton( + text: "Download Report".needTranslation, + onPressed: () async { + LoaderBottomSheet.showLoader(); + await medicalFileViewModel.getPatientMedicalReportPDF(report, appState.getAuthenticatedUser()!).then((val) async { + LoaderBottomSheet.hideLoader(); + if (medicalFileViewModel.patientMedicalReportPDFBase64.isNotEmpty) { + String path = await Utils.createFileFromString(medicalFileViewModel.patientMedicalReportPDFBase64, "pdf"); + try { + OpenFilex.open(path); + } catch (ex) { + debugPrint("Error opening file: $ex"); + } + } + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + icon: AppAssets.download, + iconColor: AppColors.primaryRedColor, + iconSize: 14.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }), + ], + ), + ) + : SizedBox.shrink(), + ), + ], + ), ), - ).paddingSymmetrical(24.h, 0.h), + ), ), ), ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation).paddingSymmetrical(24.h, 24.h); + : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation) + .paddingSymmetrical(24.h, 24.h); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), + ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 24.h), ], ); @@ -188,11 +395,10 @@ class _MedicalReportsPageState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 45.h, + borderRadius: 12.r, + height: 46.h, icon: AppAssets.requests, iconColor: AppColors.whiteColor, iconSize: 20.h, @@ -218,7 +424,9 @@ class _MedicalReportsPageState extends State { LoaderBottomSheet.showLoader(); await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: "Your medical report request has been successfully submitted.".needTranslation), callBackFunc: () { + showCommonBottomSheetWithoutHeight(context, + child: Utils.getSuccessWidget(loadingText: "Your medical report request has been successfully submitted.".needTranslation), + callBackFunc: () { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.onMedicalReportTabChange(0); medicalFileViewModel.getPatientMedicalReportList(); diff --git a/lib/presentation/medical_report/widgets/patient_medical_report_card.dart b/lib/presentation/medical_report/widgets/patient_medical_report_card.dart index 1762886..413858d 100644 --- a/lib/presentation/medical_report/widgets/patient_medical_report_card.dart +++ b/lib/presentation/medical_report/widgets/patient_medical_report_card.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -10,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_medical_response_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; diff --git a/lib/presentation/monthly_report/monthly_report.dart b/lib/presentation/monthly_report/monthly_report.dart new file mode 100644 index 0000000..1776510 --- /dev/null +++ b/lib/presentation/monthly_report/monthly_report.dart @@ -0,0 +1,189 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class MonthlyReport extends StatelessWidget { + MonthlyReport({super.key}); + + late AppState appState; + final CacheService _cacheService = GetIt.instance(); + bool isTermsAccepted = true; + + @override + Widget build(BuildContext context) { + appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer(builder: (context, monthlyReportVM, child) { + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.monthlyReports.tr(), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + LocaleKeys.patientHealthSummaryReport.tr(context: context).toText14(isBold: true), + const Spacer(), + Switch( + activeTrackColor: AppColors.successColor, + value: monthlyReportVM.isHealthSummaryEnabled, + onChanged: (newValue) async { + monthlyReportVM.setHealthSummaryEnabled(newValue); + }, + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + "${appState.getAuthenticatedUser()!.emailAddress}".toText16(color: AppColors.textColor, weight: FontWeight.w500), + ], + ), + ], + ), + ], + ).paddingSymmetrical(16.h, 16.h), + ), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.prescription_remarks_icon, width: 18.w, height: 18.h), + SizedBox(width: 9.h), + Expanded( + child: + "This monthly health summary report reflects the health indicators and analysis results of the latest visits. Please note that this will be sent automatically from the system and it’s not considered as a official report so no medical decision should be taken based on it" + .needTranslation + .toText10(weight: FontWeight.w500, color: AppColors.greyTextColorLight), + ), + ], + ), + ], + ).paddingSymmetrical(24.w, 0.h), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 32.h, left: 24.w), + child: Row( + children: [ + SizedBox( + height: 24.0, + width: 24.0, + child: Checkbox( + value: isTermsAccepted, + onChanged: (v) { + isTermsAccepted = v ?? true; + }, + activeColor: AppColors.primaryRedColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + ), + SizedBox(width: 10.w), + "I agree to the ".toText14(isBold: true, letterSpacing: -1.0), + "terms and conditions".toText14(isBold: true, letterSpacing: -1.0, color: AppColors.primaryRedColor, isUnderLine: true).onPress(() { + Utils.openWebView( + url: 'https://hmg.com/en/Pages/Terms.aspx', + ); + }) + ], + ), + ), + CustomButton( + text: LocaleKeys.save.tr(), + onPressed: () async { + LoaderBottomSheet.showLoader(loadingText: "Updating Monthly Report Status...".needTranslation); + await monthlyReportVM.updatePatientHealthSummaryReport( + rSummaryReport: monthlyReportVM.isHealthSummaryEnabled, + onSuccess: (response) async { + LoaderBottomSheet.hideLoader(); + await _cacheService.saveBool( + key: CacheConst.isMonthlyReportEnabled, + value: monthlyReportVM.isHealthSummaryEnabled, + ); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Monthly Report Status Updated Successfully".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onError: (error) { + // Error is already handled by errorHandlerService in view model + }, + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + height: 46.h, + iconColor: AppColors.whiteColor, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ), + ], + ); + }), + ); + } +} diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 781624a..07f1a4f 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -7,10 +6,8 @@ import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; -import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -19,10 +16,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; -import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; -import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; class FamilyMedicalScreen extends StatefulWidget { diff --git a/lib/presentation/my_invoices/my_invoices_details_page.dart b/lib/presentation/my_invoices/my_invoices_details_page.dart new file mode 100644 index 0000000..cccd671 --- /dev/null +++ b/lib/presentation/my_invoices/my_invoices_details_page.dart @@ -0,0 +1,272 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class MyInvoicesDetailsPage extends StatefulWidget { + GetInvoiceDetailsResponseModel getInvoiceDetailsResponseModel; + + MyInvoicesDetailsPage({super.key, required this.getInvoiceDetailsResponseModel}); + + @override + State createState() => _MyInvoicesDetailsPageState(); +} + +class _MyInvoicesDetailsPageState extends State { + late MyInvoicesViewModel myInvoicesViewModel; + + @override + Widget build(BuildContext context) { + myInvoicesViewModel = Provider.of(context, listen: false); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Invoice Details".needTranslation, + sendEmail: () async { + LoaderBottomSheet.showLoader(loadingText: "Sending email, Please wait...".needTranslation); + await myInvoicesViewModel.sendInvoiceEmail( + appointmentNo: widget.getInvoiceDetailsResponseModel.appointmentNo!, + projectID: widget.getInvoiceDetailsResponseModel.projectID!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: "Email sent successfully.".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Image.network( + widget.getInvoiceDetailsResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r), + ], + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.doctorNameN! : widget.getInvoiceDetailsResponseModel.doctorName!).toText16(isBold: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.invoiceNo}: ${widget.getInvoiceDetailsResponseModel.invoiceNo!}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: (widget.getInvoiceDetailsResponseModel.clinicDescription!.length > 15 + ? '${widget.getInvoiceDetailsResponseModel.clinicDescription!.substring(0, 12)}...' + : widget.getInvoiceDetailsResponseModel.clinicDescription!), + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + labelText: widget.getInvoiceDetailsResponseModel.projectName!, + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.getInvoiceDetailsResponseModel.appointmentDate), false), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.getInvoiceDetailsResponseModel.listConsultation!.first.procedureName!.toText16(isBold: true), + SizedBox(height: 16.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.quantity.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.quantity!}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.price.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.price!} ${LocaleKeys.sar.tr()}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.total.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.total!} ${LocaleKeys.sar.tr()}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Insurance Details".toText16(isBold: true), + SizedBox(height: 16.h), + widget.getInvoiceDetailsResponseModel.groupName!.toText14(isBold: true), + Row( + children: [ + Expanded(child: widget.getInvoiceDetailsResponseModel.companyName!.toText14(isBold: true)), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + AppCustomChipWidget( + labelText: "Insurance ID: ${widget.getInvoiceDetailsResponseModel.insuranceID ?? "-"}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + "Total Balance".needTranslation.toText18(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 17.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Amount before tax".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalShare.toString().toText16(isBold: true), AppColors.blackColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), + Utils.getPaymentAmountWithSymbol( + widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalVATAmount!.toString().toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Discount".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol(widget.getInvoiceDetailsResponseModel.listConsultation!.first.discountAmount!.toString().toText14(isBold: true, color: AppColors.primaryRedColor), + AppColors.primaryRedColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Paid".needTranslation.toText14(isBold: true), + Utils.getPaymentAmountWithSymbol( + widget.getInvoiceDetailsResponseModel.listConsultation!.first.grandTotal!.toString().toText14(isBold: true, color: AppColors.textColor), AppColors.textColor, 13, + isSaudiCurrency: true), + ], + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart new file mode 100644 index 0000000..ef1a9c2 --- /dev/null +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; +import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class MyInvoicesList extends StatefulWidget { + const MyInvoicesList({super.key}); + + @override + State createState() => _MyInvoicesListState(); +} + +class _MyInvoicesListState extends State { + late MyInvoicesViewModel myInvoicesViewModel; + + @override + void initState() { + scheduleMicrotask(() { + myInvoicesViewModel.setInvoicesListLoading(); + myInvoicesViewModel.getAllInvoicesList(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + myInvoicesViewModel = Provider.of(context, listen: false); + return CollapsingListView( + title: LocaleKeys.invoiceList.tr(context: context), + child: SingleChildScrollView( + child: Consumer(builder: (context, myInvoicesVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + ListView.builder( + itemCount: myInvoicesVM.isInvoicesListLoading ? 4 : myInvoicesVM.allInvoicesList.length, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsetsGeometry.zero, + itemBuilder: (context, index) { + return myInvoicesVM.isInvoicesListLoading + ? LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ) + : AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: InvoiceListCard( + getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], + onTap: () async { + myInvoicesVM.setInvoiceDetailLoading(); + LoaderBottomSheet.showLoader(loadingText: "Fetching invoice details, Please wait...".needTranslation); + await myInvoicesVM.getInvoiceDetails( + appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, + invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!, + projectID: myInvoicesVM.allInvoicesList[index].projectID!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: MyInvoicesDetailsPage(getInvoiceDetailsResponseModel: myInvoicesVM.invoiceDetailsResponseModel), + ), + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }, + ), + ), + ), + ), + ); + }).paddingSymmetrical(24.w, 0.h), + ], + ); + }), + ), + ); + } +} diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart new file mode 100644 index 0000000..27ca79a --- /dev/null +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class InvoiceListCard extends StatelessWidget { + final GetInvoicesListResponseModel getInvoicesListResponseModel; + Function? onTap; + + InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + alignment: WrapAlignment.start, + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + icon: AppAssets.walkin_appointment_icon, + iconColor: AppColors.textColor, + labelText: 'Walk In'.needTranslation, + textColor: AppColors.textColor, + ), + AppCustomChipWidget( + labelText: 'OutPatient'.needTranslation, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + textColor: AppColors.primaryRedColor, + ), + ], + ), + SizedBox(height: 16.h), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Image.network( + getInvoicesListResponseModel.doctorImageURL!, + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100.r), + Transform.translate( + offset: Offset(0.0, -20.h), + child: Container( + width: 40.w, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + shape: BoxShape.circle, // Makes the container circular + border: Border.all( + color: AppColors.scaffoldBgColor, // Color of the border + width: 1.5.w, // Width of the border + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + SizedBox(height: 2.h), + "${getInvoicesListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + ], + ), + ).circle(100), + ), + ], + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (getIt().isArabic() ? getInvoicesListResponseModel.doctorNameN! : getInvoicesListResponseModel.doctorName!).toText16(isBold: true), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + labelText: "${LocaleKeys.invoiceNo}: ${getInvoicesListResponseModel.invoiceNo!}", + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelText: + (getInvoicesListResponseModel.clinicName!.length > 15 ? '${getInvoicesListResponseModel.clinicName!.substring(0, 12)}...' : getInvoicesListResponseModel.clinicName!), + labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), + ), + AppCustomChipWidget( + labelText: getInvoicesListResponseModel.projectName!, + labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + ), + AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), + ), + ], + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + CustomButton( + text: "View invoice details".needTranslation, + onPressed: () { + if (onTap != null) { + onTap!(); + } + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ], + ), + ), + ).paddingOnly(bottom: 16.h); + } +} diff --git a/lib/presentation/onboarding/onboarding_screen.dart b/lib/presentation/onboarding/onboarding_screen.dart index 265559b..a40a27b 100644 --- a/lib/presentation/onboarding/onboarding_screen.dart +++ b/lib/presentation/onboarding/onboarding_screen.dart @@ -9,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; @@ -30,7 +31,13 @@ class _OnboardingScreenState extends State { void goToHomePage() { Utils.saveBoolFromPrefs(CacheConst.firstLaunch, false); - Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + // Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); } @override diff --git a/lib/presentation/onboarding/splash_animation_screen.dart b/lib/presentation/onboarding/splash_animation_screen.dart index 7acd078..105922b 100644 --- a/lib/presentation/onboarding/splash_animation_screen.dart +++ b/lib/presentation/onboarding/splash_animation_screen.dart @@ -1,10 +1,8 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; diff --git a/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart b/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart index 9f2b2a2..e2ce865 100644 --- a/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart +++ b/lib/presentation/prescriptions/prescription_delivery_orders_list_page.dart @@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; diff --git a/lib/presentation/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index a38ff92..1216c61 100644 --- a/lib/presentation/prescriptions/prescription_detail_page.dart +++ b/lib/presentation/prescriptions/prescription_detail_page.dart @@ -169,9 +169,9 @@ class _PrescriptionDetailPageState extends State { backgroundColor: AppColors.successColor.withValues(alpha: 0.15), borderColor: AppColors.successColor.withValues(alpha: 0.01), textColor: AppColors.successColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, icon: AppAssets.download, @@ -219,7 +219,15 @@ class _PrescriptionDetailPageState extends State { text: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? LocaleKeys.resendOrder.tr(context: context) : LocaleKeys.prescriptionDeliveryError.tr(context: context), - onPressed: () {}, + onPressed: () async { + if (widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported!) { + LoaderBottomSheet.showLoader(loadingText: "Fetching prescription details...".needTranslation); + await prescriptionsViewModel.getPrescriptionDetails(widget.prescriptionsResponseModel, onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + prescriptionsViewModel.initiatePrescriptionDelivery(); + }); + } + }, backgroundColor: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.greyF7Color, borderColor: AppColors.successColor.withOpacity(0.01), textColor: diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index 100d963..a2a3e7f 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -220,9 +220,9 @@ class PrescriptionItemView extends StatelessWidget { backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), borderColor: AppColors.primaryRedColor.withOpacity(0.0), textColor: AppColors.primaryRedColor, - fontSize: 13, + fontSize: 14.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ).toShimmer2(isShow: isLoading), diff --git a/lib/presentation/prescriptions/prescriptions_list_page.dart b/lib/presentation/prescriptions/prescriptions_list_page.dart index 3d631ce..8b60159 100644 --- a/lib/presentation/prescriptions/prescriptions_list_page.dart +++ b/lib/presentation/prescriptions/prescriptions_list_page.dart @@ -6,7 +6,6 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -21,10 +20,7 @@ import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_deta import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; -import 'package:hmg_patient_app_new/widgets/map/map_utility_screen.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; -import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; class PrescriptionsListPage extends StatefulWidget { @@ -130,125 +126,125 @@ class _PrescriptionsListPageState extends State { child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), - child: InkWell( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomButton( - text: "${model.patientPrescriptionOrdersViewList[index].prescriptionsList!.length} Prescriptions Available", - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 30.h, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${model.patientPrescriptionOrdersViewList[index].prescriptionsList!.length} Prescriptions Available", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], ), - Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + SizedBox(height: 8.h), + model.patientPrescriptionOrdersViewList[index].filterName!.toText16(isBold: true) ], ), - SizedBox(height: 8.h), - model.patientPrescriptionOrdersViewList[index].filterName!.toText16(isBold: true) - ], - ), - ), - AnimatedSwitcher( - duration: Duration(milliseconds: 500), - switchInCurve: Curves.easeIn, - switchOutCurve: Curves.easeOut, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: 0.0, - child: child, - ), - ); - }, - child: isExpanded - ? Container( - key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...model.patientPrescriptionOrdersViewList[index].prescriptionsList!.map((prescription) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.network( - prescription.doctorImageURL!, - width: 24.h, - height: 24.h, - fit: BoxFit.fill, - ).circle(100), - SizedBox(width: 8.h), - Expanded(child: prescription.doctorName!.toText14(weight: FontWeight.w500)), - ], - ), - SizedBox(height: 8.h), - Row( + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...model.patientPrescriptionOrdersViewList[index].prescriptionsList!.map((prescription) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomButton( - text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(prescription.appointmentDate), false), - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + prescription.doctorImageURL!, + width: 24.h, + height: 24.h, + fit: BoxFit.fill, + ).circle(100), + SizedBox(width: 8.h), + Expanded(child: prescription.doctorName!.toText14(weight: FontWeight.w500)), + ], ), - SizedBox(width: 8.h), - CustomButton( - text: model.isSortByClinic ? prescription.name! : prescription.clinicDescription!, - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, + SizedBox(height: 8.h), + Row( + children: [ + CustomButton( + text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(prescription.appointmentDate), false), + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: model.isSortByClinic ? prescription.name! : prescription.clinicDescription!, + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 24.h, + ), + ], ), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Expanded( - flex: 6, - child: CustomButton( - text: prescription.isHomeMedicineDeliverySupported! - ? LocaleKeys.resendOrder.tr(context: context) - : LocaleKeys.prescriptionDeliveryError.tr(context: context), + SizedBox(height: 8.h), + Row( + children: [ + Expanded( + flex: 6, + child: CustomButton( + text: prescription.isHomeMedicineDeliverySupported! + ? LocaleKeys.resendOrder.tr(context: context) + : LocaleKeys.prescriptionDeliveryError.tr(context: context), onPressed: () async { if (prescription.isHomeMedicineDeliverySupported!) { LoaderBottomSheet.showLoader(loadingText: "Fetching prescription details...".needTranslation); @@ -263,68 +259,68 @@ class _PrescriptionsListPageState extends State { prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor.withOpacity(0.15) : AppColors.greyF7Color, borderColor: AppColors.successColor.withOpacity(0.01), textColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), - fontSize: prescription.isHomeMedicineDeliverySupported! ? 14 : 12, + fontSize: prescription.isHomeMedicineDeliverySupported! ? 14.f : 12.f, fontWeight: FontWeight.w500, - borderRadius: 12, + borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, icon: AppAssets.prescription_refill_icon, - iconColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), - iconSize: 14.h, - ), - ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( + iconColor: prescription.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.textColor.withOpacity(0.35), + iconSize: 16.h, + ), + ), + SizedBox(width: 8.h), + Expanded( + flex: 1, + child: Container( height: 40.h, width: 40.w, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, + color: AppColors.textColor, borderRadius: 12, ), - child: Padding( + child: Padding( padding: EdgeInsets.all(12.h), child: Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.whiteColor, + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.whiteColor, fit: BoxFit.contain, + ), + ), ), - ), - ), - ).onPress(() { - model.setPrescriptionsDetailsLoading(); - Navigator.of(context).push( - CustomPageRoute( + ).onPress(() { + model.setPrescriptionsDetailsLoading(); + Navigator.of(context).push( + CustomPageRoute( page: PrescriptionDetailPage( prescriptionsResponseModel: prescription, isFromAppointments: false, ), ), ); - }), + }), + ), + ], ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), ], - ), - SizedBox(height: 12.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), - SizedBox(height: 12.h), - ], - ); - }).toList(), - ], - ), - ) - : SizedBox.shrink(), + ); + }).toList(), + ], + ), + ) + : SizedBox.shrink(), + ), + ], ), - ], + ), ), ), ), - ), - ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any prescriptions yet.".needTranslation); }, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 9e4c808..e463ae7 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_swiper_view/flutter_swiper_view.dart'; @@ -28,6 +30,7 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class ProfileSettings extends StatefulWidget { const ProfileSettings({super.key}); @@ -195,8 +198,6 @@ class ProfileSettingsState extends State { title: "Application Language".needTranslation, child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false); }, trailingLabel: Utils.appState.isArabic() ? "العربية".needTranslation : "English".needTranslation), 1.divider, - actionItem(AppAssets.accessibility, "Symptoms Checker".needTranslation, () {}), - 1.divider, actionItem(AppAssets.accessibility, "Accessibility".needTranslation, () {}), 1.divider, actionItem(AppAssets.bell, "Notifications Settings".needTranslation, () {}), @@ -235,15 +236,35 @@ class ProfileSettingsState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: Column( children: [ - actionItem(AppAssets.call_fill, "Contact Us".needTranslation, () {}, trailingLabel: "9200666666"), + actionItem(AppAssets.call_fill, "Contact Us".needTranslation, () { + launchUrl(Uri.parse("tel://" + "+966 11 525 9999")); + }, trailingLabel: "011 525 9999"), 1.divider, actionItem(AppAssets.permission, "Permissions".needTranslation, () {}, trailingLabel: "Location, Camera"), 1.divider, - actionItem(AppAssets.rate, "Rate Our App".needTranslation, () {}, isExternalLink: true), + actionItem(AppAssets.rate, "Rate Our App".needTranslation, () { + if (Platform.isAndroid) { + Utils.openWebView( + url: 'https://play.google.com/store/apps/details?id=com.ejada.hmg', + ); + } else { + Utils.openWebView( + url: 'https://itunes.apple.com/app/id733503978', + ); + } + }, isExternalLink: true), 1.divider, - actionItem(AppAssets.privacy_terms, "Privacy Policy".needTranslation, () {}, isExternalLink: true), + actionItem(AppAssets.privacy_terms, "Privacy Policy".needTranslation, () { + Utils.openWebView( + url: 'https://hmg.com/en/Pages/Privacy.aspx', + ); + }, isExternalLink: true), 1.divider, - actionItem(AppAssets.privacy_terms, "Terms & Conditions".needTranslation, () {}, isExternalLink: true), + actionItem(AppAssets.privacy_terms, "Terms & Conditions".needTranslation, () { + Utils.openWebView( + url: 'https://hmg.com/en/Pages/Terms.aspx', + ); + }, isExternalLink: true), ], ), ), diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index cb925ef..fb153ea 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; +import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patient_radiology_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.dart'; @@ -22,6 +22,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../features/radiology/radiology_view_model.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; class RadiologyOrdersPage extends StatefulWidget { const RadiologyOrdersPage({super.key}); @@ -34,226 +35,343 @@ class _RadiologyOrdersPageState extends State { late RadiologyViewModel radiologyViewModel; String selectedFilterText = ''; int? expandedIndex; + // Scroll controller to ensure expanded group is visible + late ScrollController _scrollController; + final Map _groupKeys = {}; @override void initState() { + _scrollController = ScrollController(); scheduleMicrotask(() { radiologyViewModel.initRadiologyViewModel(); }); super.initState(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { radiologyViewModel = Provider.of(context); return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.radiology.tr(context: context), - search: () async { - final lavVM = Provider.of(context, listen: false); - if (lavVM.isLabOrdersLoading) { - return; - } else { - String? value = await Navigator.of(context).push( - CustomPageRoute( - page: SearchRadiologyContent(radiologySuggestionsList: radiologyViewModel.radiologySuggestions), - fullScreenDialog: true, - direction: AxisDirection.down, - ), - ); - if (value != null) { - selectedFilterText = value; - radiologyViewModel.filterRadiologyReports(value); + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.radiology.tr(context: context), + search: () async { + final lavVM = Provider.of(context, listen: false); + if (lavVM.isLabOrdersLoading) { + return; + } else { + String? value = await Navigator.of(context).push( + CustomPageRoute( + page: SearchRadiologyContent(radiologySuggestionsList: radiologyViewModel.radiologySuggestions), + fullScreenDialog: true, + direction: AxisDirection.down, + ), + ); + if (value != null) { + selectedFilterText = value; + radiologyViewModel.filterRadiologyReports(value); + } } - } - }, - child: SingleChildScrollView( - child: Consumer( - builder: (context, model, child) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - selectedFilterText!.isNotEmpty - ? AppCustomChipWidget( - padding: EdgeInsets.symmetric(horizontal: 5.h), - labelText: selectedFilterText!, - deleteIcon: 'assets/images/svg/cross_circle.svg', - backgroundColor: AppColors.alertColor, - textColor: AppColors.whiteColor, - deleteIconColor: AppColors.whiteColor, - deleteIconHasColor: true, - onDeleteTap: () { - setState(() { - selectedFilterText = ''; - model.filterRadiologyReports(''); - }); + }, + child: SingleChildScrollView( + controller: _scrollController, + physics: NeverScrollableScrollPhysics(), + child: Consumer( + builder: (context, model, child) { + // Build grouping lists if we have data and none constructed yet + if (!model.isRadiologyOrdersLoading && model.patientRadiologyOrders.isNotEmpty && model.patientRadiologyOrdersViewList.isEmpty) { + final clinicMap = >{}; + final hospitalMap = >{}; + for (var order in model.patientRadiologyOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + model.patientRadiologyOrdersByClinic = clinicMap.values.toList(); + model.patientRadiologyOrdersByHospital = hospitalMap.values.toList(); + model.patientRadiologyOrdersViewList = model.isSortByClinic ? model.patientRadiologyOrdersByClinic : model.patientRadiologyOrdersByHospital; + } + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Clinic / Hospital toggle + SizedBox(height: 16.h), + Row( + children: [ + CustomButton( + text: LocaleKeys.byClinic.tr(context: context), + onPressed: () { + model.setIsSortByClinic(true); }, - // chipType: ChipTypeEnum.alert, - // isSelected: true, - ) - : SizedBox(), - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: model.isRadiologyOrdersLoading - ? 5 - : model.patientRadiologyOrders.isNotEmpty - ? model.patientRadiologyOrders.length - : 1, - itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; - return model.isRadiologyOrdersLoading - ? LabResultItemView( - onTap: () {}, - labOrder: null, - index: index, - isLoading: true, - ) - : model.patientRadiologyOrders.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), - child: InkWell( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - }, + backgroundColor: model.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), + textColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.byHospital.tr(context: context), + onPressed: () { + model.setIsSortByClinic(false); + }, + backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, + textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + fontSize: 12, + fontWeight: FontWeight.w500, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ), + + SizedBox(height: 8.h), + selectedFilterText.isNotEmpty + ? AppCustomChipWidget( + padding: EdgeInsets.symmetric(horizontal: 5.h), + labelText: selectedFilterText, + deleteIcon: 'assets/images/svg/cross_circle.svg', + backgroundColor: AppColors.alertColor, + textColor: AppColors.whiteColor, + deleteIconColor: AppColors.whiteColor, + deleteIconHasColor: true, + onDeleteTap: () { + setState(() { + selectedFilterText = ''; + model.filterRadiologyReports(''); + }); + }, + // chipType: ChipTypeEnum.alert, + // isSelected: true, + ) + : SizedBox(), + + // Grouped view when available + Builder(builder: (ctx) { + if (model.isRadiologyOrdersLoading) { + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: 5, + itemBuilder: (context, index) => LabResultItemView( + onTap: () {}, + labOrder: null, + index: index, + isLoading: true, + ), + ); + } + + if (model.patientRadiologyOrdersViewList.isEmpty) { + return Utils.getNoDataWidget(ctx, noDataText: "You don't have any radiology results yet.".needTranslation); + } + + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: model.patientRadiologyOrdersViewList.length, + itemBuilder: (context, index) { + final group = model.patientRadiologyOrdersViewList[index]; + final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); + final isExpanded = expandedIndex == index; + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 400), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); + if (key.currentContext != null && expandedIndex == index) { + Future.delayed(const Duration(milliseconds: 450), () { + if (key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.0, + ); + } + }); + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + key: _groupKeys.putIfAbsent(index, () => GlobalKey()), + padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppCustomChipWidget( - labelText: LocaleKeys.resultsAvailable.tr(context: context), - backgroundColor: AppColors.successColor.withOpacity(0.15), - textColor: AppColors.successColor, - ).toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 100), - SizedBox(height: 8.h), - Row( - children: [ - Image.network( - model.isRadiologyOrdersLoading - ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" - : model.patientRadiologyOrders[index].doctorImageURL!, - width: 24.h, - height: 24.h, - fit: BoxFit.fill, - ).circle(100).toShimmer2(isShow: model.isRadiologyOrdersLoading), - SizedBox(width: 4.h), - (model.isRadiologyOrdersLoading - ? "Dr John Smith" - : model.patientRadiologyOrders[index].doctorName!) - .toText16(isBold: true) - .toShimmer2(isShow: model.isRadiologyOrdersLoading) - ], - ), - SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, - spacing: 3.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: model.isRadiologyOrdersLoading - ? "01 Jan 2025" - : DateUtil.formatDateToDate(model.patientRadiologyOrders[index].orderDate!, false), - ).toShimmer2(isShow: model.isRadiologyOrdersLoading), - AppCustomChipWidget( - labelText: model.isRadiologyOrdersLoading - ? "01 Jan 2025" - : model.patientRadiologyOrders[index].clinicDescription!, - ).toShimmer2(isShow: model.isRadiologyOrdersLoading), - - // AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h), - // AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h), - ], - ), - ], - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], ), - model.isRadiologyOrdersLoading - ? SizedBox.shrink() - : AnimatedCrossFade( - firstChild: SizedBox.shrink(), - secondChild: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( + SizedBox(height: 8.h), + Text( + displayName, + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...group.map((order) { + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: '● ${model.patientRadiologyOrders[index].description}' - .toText14(weight: FontWeight.w500), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 24.w, + height: 24.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Expanded( + child: (order.doctorName ?? '').toString().toText14(weight: FontWeight.w500), + ), + ], ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + if ((order.description ?? '').isNotEmpty) + AppCustomChipWidget( + labelText: (order.description ?? '').toString(), + ), + AppCustomChipWidget( + labelText: DateUtil.formatDateToDate( + (order.orderDate ?? order.appointmentDate), + false, + ), + ), + AppCustomChipWidget( + labelText: model.isSortByClinic ? (order.clinicDescription ?? '') : (order.projectName ?? ''), + ), + ], + ), + SizedBox(height: 12.h), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox(), - CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewReport.tr(context: context), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: RadiologyResultPage( - patientRadiologyResponseModel: model.patientRadiologyOrders[index]), - ), - ); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14, - fontWeight: FontWeight.bold, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, + Expanded(flex: 2, child: const SizedBox()), + Expanded( + flex: 2, + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: "View Results".needTranslation, + onPressed: () { + model.navigationService.push( + CustomPageRoute( + page: RadiologyResultPage(patientRadiologyResponseModel: order), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), ), ], ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), ], - ), - ), - crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: Duration(milliseconds: 300), - ), - ], - ), + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), ), - ), + ], ), ), - ) - : Utils.getNoDataWidget(context, noDataText: "You don't have any radiology results yet.".needTranslation); - }, - ), - ], - ), - ); - }, + ), + ), + ), + ); + }, + ); + }), + ], + ), + ); + }, + ), ), ), - ), - ); + ); } Color getLabOrderStatusColor(num status) { diff --git a/lib/presentation/radiology/radiology_result_page.dart b/lib/presentation/radiology/radiology_result_page.dart index df34164..1fc2d9f 100644 --- a/lib/presentation/radiology/radiology_result_page.dart +++ b/lib/presentation/radiology/radiology_result_page.dart @@ -10,7 +10,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patient_radiology_response_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; diff --git a/lib/presentation/radiology/search_radiology.dart b/lib/presentation/radiology/search_radiology.dart index 98f5c90..8d63a59 100644 --- a/lib/presentation/radiology/search_radiology.dart +++ b/lib/presentation/radiology/search_radiology.dart @@ -8,7 +8,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:sizer/sizer.dart'; class SearchRadiologyContent extends StatefulWidget { final List radiologySuggestionsList; diff --git a/lib/presentation/rate_appointment/rate_appointment_clinic.dart b/lib/presentation/rate_appointment/rate_appointment_clinic.dart new file mode 100644 index 0000000..5fb1fa3 --- /dev/null +++ b/lib/presentation/rate_appointment/rate_appointment_clinic.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class RateAppointmentClinic extends StatefulWidget { + + late final String? doctorNote; + late final int? doctorRate; + + RateAppointmentClinic({this.doctorRate, this.doctorNote}); + + @override + _RateAppointmentClinicState createState() => _RateAppointmentClinicState(); +} + +class _RateAppointmentClinicState extends State { + final formKey = GlobalKey(); + String note = ""; + int rating = 5; + AppointmentRatingViewModel? appointmentRatingViewModel; + MyAppointmentsViewModel? myAppointmentsViewModel; + + @override + Widget build(BuildContext context) { + myAppointmentsViewModel = Provider.of(context, listen: false); + appointmentRatingViewModel = Provider.of(context, listen: false); + + // Make the sheet a fixed height and keep content scrollable while pinning buttons to bottom + final sheetHeight = ResponsiveExtension.screenHeight * 0.60; + + return SizedBox( + height: sheetHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable content + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor row + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: BuildDoctorRow( + isForClinic: true, + appointmentDetails: appointmentRatingViewModel!.appointmentDetails, + ), + ), + SizedBox(height: 16), + + // Rate clinic box + SizedBox( + width: double.infinity, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + "Rate Clinic".needTranslation.toText16(isBold: true), + + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...List.generate( + 5, + (index) => rating == (index + 1) + ? Container( + margin: EdgeInsets.only(left: 3.0, right: 3.0), + child: IconButton( + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + iconSize: 35, + icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg', colorFilter: getColors(rating)), + ), + ) + : IconButton( + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + iconSize: 35, + icon: SvgPicture.asset('assets/images/svg/rate_${index + 1}.svg'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + + SizedBox(height: 12), + + // Extra content area (keeps any other widgets that were previously below) + Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Placeholder for in-content widgets if needed in future + ], + ), + ), + + // Add bottom spacing so last content isn't obscured by the fixed buttons + SizedBox(height: 12), + ], + ), + ), + + ), + + // Bottom action buttons pinned to bottom of the sheet + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric( vertical: 12.0), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Back".needTranslation, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + onPressed: () { + appointmentRatingViewModel!.setTitle("Rate Doctor".needTranslation); + appointmentRatingViewModel!.setSubTitle("How was your last visit with doctor?".needTranslation); + appointmentRatingViewModel!.setClinicOrDoctor(false); + setState(() { + + }); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: CustomButton( + text: "Submit".needTranslation, + onPressed: () { + + submitRating(); + + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + ColorFilter getColors(int rating){ + + switch(rating){ + case 5: + return ColorFilter.mode(AppColors.bgGreenColor, BlendMode.srcIn); + case 4: + return ColorFilter.mode(Colors.greenAccent, BlendMode.srcIn); + case 3: + return ColorFilter.mode(AppColors.warningLightColor, BlendMode.srcIn); + case 2: + return ColorFilter.mode(Colors.orange, BlendMode.srcIn); + case 1: + return ColorFilter.mode(AppColors.primaryRedColor, BlendMode.srcIn); + + default: + return ColorFilter.mode(AppColors.greyColor, BlendMode.srcIn); + } + } + + submitRating() async{ + LoaderBottomSheet.showLoader(); + await appointmentRatingViewModel!.submitDoctorRating(docRate: widget.doctorRate!, docNote: widget.doctorNote!); + await appointmentRatingViewModel!.submitClinicRating(clinicRate: rating, clinicNote: note); + LoaderBottomSheet.hideLoader(); + Navigator.pop(context); + } + +} diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart new file mode 100644 index 0000000..ac79744 --- /dev/null +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_clinic.dart'; +import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; + +class RateAppointmentDoctor extends StatefulWidget { + + bool isFromRegistration; + + RateAppointmentDoctor({Key? key, this.isFromRegistration = false}) : super(key: key); + + @override + _RateAppointmentDoctorState createState() => _RateAppointmentDoctorState(); +} + +class _RateAppointmentDoctorState extends State { + final formKey = GlobalKey(); + String note = ""; + int rating = 5; + + // ProjectViewModel? projectViewModel; + AppointmentRatingViewModel? appointmentRatingViewModel; + MyAppointmentsViewModel? myAppointmentsViewModel; + + @override + void initState() { + + super.initState(); + } + + + @override + Widget build(BuildContext context) { + + myAppointmentsViewModel = Provider.of(context, listen: false); + appointmentRatingViewModel = Provider.of(context, listen: false); + + final sheetHeight = ResponsiveExtension.screenHeight * 0.60; + + return Selector( + selector: (_, vm) => vm.isRateClinic, + builder: (context, isRateClinic, child) => isRateClinic + ? RateAppointmentClinic(doctorNote: note, doctorRate: rating,) + : SizedBox( + height: sheetHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable main content + Expanded( + + child: Padding( + padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Doctor row + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: BuildDoctorRow( + isForClinic: false, + appointmentDetails: appointmentRatingViewModel!.appointmentDetails, + )), + + SizedBox(height: 16), + + // Rating box + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + "Please rate the doctor".needTranslation.toText16(isBold: true), + + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...List.generate( + 5, + (index) => AnimatedSwitcher( + duration: Duration(milliseconds: 1000), + switchInCurve: Curves.elasticOut, + switchOutCurve: Curves.elasticIn, + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(child: child, scale: animation); + }, + child: Container( + key: ValueKey(rating), + child: IconButton( + iconSize: 45.0, + onPressed: () { + setState(() { + rating = index + 1; + }); + }, + color: rating >= (index + 1) + ? Color.fromRGBO(255, 186, 0, 1.0) + : Colors.grey[400], + icon: Icon(rating >= (index + 1) ? Icons.star : Icons.star)), + ), + ), + ) + ], + ), + ], + ), + ), + ), + + SizedBox(height: 12), + + // Note text field + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.0), + child: TextField( + + maxLines: 4, + decoration: InputDecoration.collapsed( + hintText: "Notes".needTranslation, + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xff2B353E), + letterSpacing: -0.64, + height: 23 / 16)), + onChanged: (value) { + setState(() { + note = value; + }); + }, + ))), + + + ], + ), + ), + + ), + + // Bottom action buttons pinned to bottom + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Later".needTranslation, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: CustomButton( + text: "Next".needTranslation, + onPressed: () { + // Set up clinic rating and show clinic rating view + appointmentRatingViewModel!.setTitle("Rate Clinic".needTranslation); + appointmentRatingViewModel!.setSubTitle("How was your appointment?".needTranslation); + appointmentRatingViewModel!.setClinicOrDoctor(true); + + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + ], + ), + )); + + // DoctorList getDoctorObject(AppointmentRateViewModel model) { + // DoctorList doctor = new DoctorList(); + // + // doctor.name = model.appointmentDetails.doctorName; + // doctor.doctorImageURL = model.appointmentDetails.doctorImageURL; + // doctor.clinicName = model.appointmentDetails.clinicName; + // doctor.projectName = model.appointmentDetails.projectName; + // doctor.date = model.appointmentDetails.appointmentDate; + // doctor.actualDoctorRate = 5; + // + // return doctor; + // } + } + + +} diff --git a/lib/presentation/rate_appointment/widget/doctor_row.dart b/lib/presentation/rate_appointment/widget/doctor_row.dart new file mode 100644 index 0000000..725ac7c --- /dev/null +++ b/lib/presentation/rate_appointment/widget/doctor_row.dart @@ -0,0 +1,94 @@ + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class BuildDoctorRow extends StatelessWidget { + bool isForClinic = false; + AppointmentDetails? appointmentDetails; + + BuildDoctorRow({super.key, required this.isForClinic, this.appointmentDetails}); + + @override + Widget build(BuildContext context) { + + return Padding(padding: EdgeInsets.all(16),child:Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL , + width: 63.h, + height: 63.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 16.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (isForClinic ? appointmentDetails!.projectName : appointmentDetails!.doctorName)!.toString() + .toText16(isBold: true, maxlines: 1), + + SizedBox(height: 8.h), + + + isForClinic ? Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + + labelText: + appointmentDetails!.clinicName.toString(), + + ), + AppCustomChipWidget( + icon: AppAssets.ic_date_filter, + labelText: + DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointmentDetails!.appointmentDate), false), + + ), + + AppCustomChipWidget( + icon: AppAssets.appointment_time_icon, + labelText: + appointmentDetails!.startTime.substring(0, appointmentDetails!.startTime.length - 3), + + ), + + ] + ) : Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + + labelText: + appointmentDetails!.projectName.toString(), + + + ), + AppCustomChipWidget( + + labelText: + appointmentDetails!.clinicName.toString(), + + ) + + ] + ) + ], + ), + ), + ], + )); + } + +} \ No newline at end of file diff --git a/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart new file mode 100644 index 0000000..7c537ba --- /dev/null +++ b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart @@ -0,0 +1,419 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:provider/provider.dart'; +import 'package:health/health.dart'; + +class HealthDashboard extends StatefulWidget { + const HealthDashboard({Key? key}) : super(key: key); + + @override + _HealthDashboardState createState() => _HealthDashboardState(); +} + +class _HealthDashboardState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + final dateFormat = DateFormat('MMM dd, yyyy'); + final timeFormat = DateFormat('hh:mm a'); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + Future.microtask( + () async { + await Health().configure(); + context.read().fetchHealthData(); + }, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Consumer( + builder: (context, healthProvider, child) { + return SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 96.h), + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, "Overview".needTranslation), + CustomTabBarModel(null, "Details".needTranslation), + ], + onTabChange: (index) { + healthProvider.onTabChanged(index); + }, + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + getSelectedTabData(healthProvider.selectedTabIndex, healthProvider).paddingSymmetrical(24.w, 0), + ], + ), + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => context.read().fetchHealthData(), + tooltip: 'Refresh health data', + child: Icon(Icons.refresh), + ), + ); + } + + Widget getSelectedTabData(int index, HealthProvider healthProvider) { + switch (index) { + case 0: + return _buildOverviewTab(healthProvider); + case 1: + return _buildDetailsTab(healthProvider); + default: + SizedBox.shrink(); + } + return Container(); + } + + Widget _buildOverviewTab(HealthProvider healthProvider) { + if (healthProvider.isLoading) { + return _buildLoadingState(); + } + + if (healthProvider.error != null) { + return _buildErrorState(healthProvider.error!); + } + return CustomScrollView( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: _buildTimeRangeSelector(healthProvider), + ), + SliverPadding( + padding: EdgeInsets.symmetric(vertical: 12.w), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 0.62, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final metric = HealthMetrics.metrics[index]; + return HealthMetricCard( + metric: metric, + data: healthProvider.healthData[metric.type] ?? [], + onTap: () => _showMetricDetails(context, metric, healthProvider), + ); + }, + childCount: HealthMetrics.metrics.length, + ), + ), + ), + ], + ); + } + + Widget _buildDetailsTab(HealthProvider healthProvider) { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: HealthMetrics.metrics.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final metric = HealthMetrics.metrics[index]; + final data = healthProvider.healthData[metric.type] ?? []; + + return Card( + color: AppColors.whiteColor, + margin: EdgeInsets.only(bottom: 16), + child: ExpansionTile( + leading: Icon( + metric.icon, + color: metric.color, + size: 32.h, + ), + // title: Text(metric.name), + title: (getIt.get().isArabic() ? metric.nameAr : metric.nameEn).toText14(isBold: true), + subtitle: Text( + data.isEmpty ? LocaleKeys.noDataAvailable.tr(context: context) : '${_getValueAsDouble(data.last.value, metric.type).toStringAsFixed(1)} ${metric.unit}', + ), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + metric.description.toText13(), + SizedBox(height: 16.h), + if (data.isNotEmpty) ...[ + Text( + 'History', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + SizedBox(height: 8), + ...data.reversed + .take(5) + .map((point) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + dateFormat.format(point.dateFrom), + style: TextStyle(color: Colors.grey[600]), + ), + Text( + '${_getValueAsDouble(point.value, metric.type).toStringAsFixed(1)} ${metric.unit}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + )) + .toList(), + ], + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildTimeRangeSelector(HealthProvider provider) { + return Container( + height: 40, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (final range in ['1D', '7D', '1M', '3M', '1Y']) + Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(range), + labelStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + checkmarkColor: AppColors.primaryRedColor, + selected: provider.selectedTimeRange == range, + selectedColor: AppColors.secondaryLightRedColor, + onSelected: (selected) { + if (selected) { + provider.updateTimeRange(range); + } + }, + ), + ), + ], + ), + ); + } + + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading health data...'), + ], + ), + ); + } + + Widget _buildErrorState(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Text( + 'Error loading health data', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + error, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[600]), + ), + SizedBox(height: 24), + ElevatedButton( + onPressed: () => context.read().fetchHealthData(), + child: Text('Try Again'), + ), + ], + ), + ); + } + + void _showMetricDetails( + BuildContext context, + HealthMetricInfo metric, + HealthProvider provider, + ) { + final data = provider.healthData[metric.type] ?? []; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (context, scrollController) => Container( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + margin: EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Row( + children: [ + Icon(metric.icon, color: metric.color, size: 30), + SizedBox(width: 16), + (getIt.get().isArabic() ? metric.nameAr : metric.nameEn).toText24(isBold: true), + // Text( + // metric.name, + // style: TextStyle( + // fontSize: 24, + // fontWeight: FontWeight.bold, + // ), + // ), + ], + ), + SizedBox(height: 16), + Text( + metric.description, + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + SizedBox(height: 24), + Text( + 'Healthy Range', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + '${metric.minHealthyValue} - ${metric.maxHealthyValue} ${metric.unit}'.toText16(isBold: true, color: AppColors.primaryRedColor), + // Text( + // '${metric.minHealthyValue} - ${metric.maxHealthyValue} ${metric.unit}', + // style: TextStyle( + // color: mainPurple, + // fontSize: 16, + // ), + // ), + SizedBox(height: 24), + if (data.isNotEmpty) ...[ + Text( + 'History', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 16), + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: data.length, + itemBuilder: (context, index) { + final point = data[data.length - 1 - index]; + final value = _getValueAsDouble(point.value, metric.type); + return Card( + child: ListTile( + title: Text( + '${value.toStringAsFixed(1)} ${metric.unit}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + // dateFormat.format(point.dateFrom), + Utils.getMonthDayYearDateFormatted(point.dateFrom) + // dateFormat.format(point.dateFrom), + ), + trailing: Icon( + value >= metric.minHealthyValue && value <= metric.maxHealthyValue ? Icons.check_circle : Icons.warning, + color: value >= metric.minHealthyValue && value <= metric.maxHealthyValue ? AppColors.textColor : Colors.orange, + ), + ), + ); + }, + ), + ), + ] else + Center( + child: Text( + 'No data available', + style: TextStyle(color: Colors.grey[600]), + ), + ), + ], + ), + ), + ), + ); + } + + // Add this helper method to the _HealthDashboardState class + double _getValueAsDouble(HealthValue value, HealthDataType type) { + if (value is NumericHealthValue) { + // if(type == HealthDataType.BLOOD_OXYGEN) { + // return (value.numericValue.toDouble() * 100); + // } else { + return value.numericValue.toDouble(); + // } + } + return 0.0; + } +} diff --git a/lib/presentation/smartwatches/huawei_health_example.dart b/lib/presentation/smartwatches/huawei_health_example.dart new file mode 100644 index 0000000..4163d8b --- /dev/null +++ b/lib/presentation/smartwatches/huawei_health_example.dart @@ -0,0 +1,1563 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:huawei_health/huawei_health.dart'; + +const String packageName = 'com.ejada.hmg'; + +class HuaweiHealthExample extends StatefulWidget { + const HuaweiHealthExample({Key? key}) : super(key: key); + + @override + State createState() => _HuaweiHealthExampleState(); +} + +class _HuaweiHealthExampleState extends State { + /// Styles + static const TextStyle cardTitleTextStyle = TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + ); + static const EdgeInsets componentPadding = EdgeInsets.all(8.0); + + /// Text Controllers for showing the logs of different modules + final TextEditingController _activityTextController = TextEditingController(); + final TextEditingController _dataTextController = TextEditingController(); + final TextEditingController _settingTextController = TextEditingController(); + final TextEditingController _autoRecorderTextController = TextEditingController(); + final TextEditingController _consentTextController = TextEditingController(); + final TextEditingController _healthTextController = TextEditingController(); + + /// Data controller reference to initialize at startup. + late DataController _dataController; + + String? accessToken = ''; + + @override + void initState() { + super.initState(); + if (!mounted) return; + // Initialize Event Callbacks + AutoRecorderController.autoRecorderStream.listen(_onAutoRecorderEvent); + // Initialize a DataController + initDataController(); + } + + /// Prints the specified text on both the console and the specified text controller. + void log( + String methodName, + TextEditingController controller, + LogOptions logOption, { + String? result = '', + String? error = '', + }) { + String log = ''; + switch (logOption) { + case LogOptions.call: + log = '$methodName called'; + break; + case LogOptions.success: + log = '$methodName [Success: $result] '; + break; + case LogOptions.error: + log = '$methodName [Error: $error] [Error Description: ${HiHealthStatusCodes.getStatusCodeMessage(error ?? '')}]'; + break; + case LogOptions.custom: + log = methodName; // Custom text + break; + } + debugPrint(log); + setState(() { + controller.text = '$log\n${controller.text}'; + }); + } + + /// Authorizes Huawei Health Kit for the user, with defined scopes. + void signIn() async { + // List of scopes to ask for authorization. + // + // Note: These scopes should also be authorized on the Huawei Developer Console. + final List scopes = [ + Scope.HEALTHKIT_ACTIVITY_READ, + Scope.HEALTHKIT_ACTIVITY_WRITE, + Scope.HEALTHKIT_BLOODGLUCOSE_READ, + Scope.HEALTHKIT_BLOODGLUCOSE_WRITE, + Scope.HEALTHKIT_CALORIES_READ, + Scope.HEALTHKIT_CALORIES_WRITE, + Scope.HEALTHKIT_DISTANCE_READ, + Scope.HEALTHKIT_DISTANCE_WRITE, + Scope.HEALTHKIT_HEARTRATE_READ, + Scope.HEALTHKIT_HEARTRATE_WRITE, + Scope.HEALTHKIT_HEIGHTWEIGHT_READ, + Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, + Scope.HEALTHKIT_LOCATION_READ, + Scope.HEALTHKIT_LOCATION_WRITE, + Scope.HEALTHKIT_PULMONARY_READ, + Scope.HEALTHKIT_PULMONARY_WRITE, + Scope.HEALTHKIT_SLEEP_READ, + Scope.HEALTHKIT_SLEEP_WRITE, + Scope.HEALTHKIT_SPEED_READ, + Scope.HEALTHKIT_SPEED_WRITE, + Scope.HEALTHKIT_STEP_READ, + Scope.HEALTHKIT_STEP_WRITE, + Scope.HEALTHKIT_STRENGTH_READ, + Scope.HEALTHKIT_STRENGTH_WRITE, + Scope.HEALTHKIT_BODYFAT_READ, + Scope.HEALTHKIT_BODYFAT_WRITE, + Scope.HEALTHKIT_NUTRITION_READ, + Scope.HEALTHKIT_NUTRITION_WRITE, + Scope.HEALTHKIT_BLOODPRESSURE_READ, + Scope.HEALTHKIT_BLOODPRESSURE_WRITE, + Scope.HEALTHKIT_BODYTEMPERATURE_READ, + Scope.HEALTHKIT_BODYTEMPERATURE_WRITE, + Scope.HEALTHKIT_OXYGENSTATURATION_READ, + Scope.HEALTHKIT_OXYGENSTATURATION_WRITE, + Scope.HEALTHKIT_REPRODUCTIVE_READ, + Scope.HEALTHKIT_REPRODUCTIVE_WRITE, + Scope.HEALTHKIT_ACTIVITY_RECORD_READ, + Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, + Scope.HEALTHKIT_HEARTRATE_REALTIME, + Scope.HEALTHKIT_STEP_REALTIME, + Scope.HEALTHKIT_HEARTHEALTH_WRITE, + Scope.HEALTHKIT_HEARTHEALTH_READ, + Scope.HEALTHKIT_STRESS_WRITE, + Scope.HEALTHKIT_STRESS_READ, + Scope.HEALTHKIT_OXYGEN_SATURATION_WRITE, + Scope.HEALTHKIT_OXYGEN_SATURATION_READ, + Scope.HEALTHKIT_HISTORYDATA_OPEN_WEEK, + Scope.HEALTHKIT_HISTORYDATA_OPEN_MONTH, + Scope.HEALTHKIT_HISTORYDATA_OPEN_YEAR, + ]; + try { + AuthHuaweiId? result = await HealthAuth.signIn(scopes); + debugPrint( + 'Granted Scopes for User(${result?.displayName}): ${result?.grantedScopes?.toString()}', + ); + showSnackBar( + 'Authorization Success.', + color: Colors.green, + ); + setState(() => accessToken = result?.accessToken); + } on PlatformException catch (e) { + debugPrint('Error on authorization, Error:${e.toString()}'); + showSnackBar( + 'Error on authorization, Error:${e.toString()}, Error Description: ' + '${HiHealthStatusCodes.getStatusCodeMessage(e.message ?? '')}', + ); + } + } + + // ActivityRecordsController + // + /// Adds an ActivityRecord with an ActivitySummary, time range is 2 hours from now. + Future addActivityRecord() async { + log( + 'addActivityRecord', + _activityTextController, + LogOptions.call, + ); + DateTime startTime = DateTime.now().subtract(const Duration(hours: 2)); + DateTime endTime = DateTime.now(); + // Build an ActivityRecord object + ActivityRecord activityRecord = ActivityRecord( + startTime: startTime, + endTime: endTime, + id: 'ActivityRecordId0', + name: 'AddActivityRecord', + activityTypeId: HiHealthActivities.running, + description: 'This is a test for ActivityRecord', + activitySummary: ActivitySummary( + paceSummary: PaceSummary( + avgPace: 247.27626, + bestPace: 212.0, + britishPaceMap: { + '102802480': 365.0, + }, + britishPartTimeMap: { + '1.0': 263.0, + }, + partTimeMap: { + '1.0': 456.0, + }, + paceMap: { + '1.0': 263.0, + }, + ), + dataSummary: [ + SamplePoint( + dataType: DataType.DT_CONTINUOUS_DISTANCE_TOTAL, + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + fieldValueOptions: FieldFloat(Field.FIELD_DISTANCE, 400), + timeUnit: TimeUnit.MILLISECONDS, + ), + SamplePoint( + dataType: DataType.POLYMERIZE_CONTINUOUS_SPEED_STATISTICS, + fieldValueOptions: FieldFloat(Field.FIELD_AVG, 60.0), + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + timeUnit: TimeUnit.MILLISECONDS, + ) + ..setFieldValue(Field.FIELD_MIN, 40.0) + ..setFieldValue(Field.FIELD_MAX, 80.0), + ]), + ); + + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, + name: 'AddActivityRecord1923', + ); + + // You can use sampleSets to add more sample points to the sampling dataset. + // Build a list of sampling point objects and add it to the sampling dataSet + List samplePoints = [ + SamplePoint( + dataCollector: dataCollector, + startTime: startTime.add(Duration(seconds: 1)), + endTime: endTime.subtract(Duration(seconds: 1)), + fieldValueOptions: FieldFloat(Field.FIELD_STEP_RATE, 10.0), + timeUnit: TimeUnit.MILLISECONDS, + ), + ]; + SampleSet sampleSet = SampleSet( + dataCollector, + samplePoints, + ); + + try { + await ActivityRecordsController.addActivityRecord( + ActivityRecordInsertOptions( + activityRecord: activityRecord, + sampleSets: [ + sampleSet, + ], + ), + ); + log( + 'addActivityRecord', + _activityTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'addActivityRecord', + _activityTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Obtains saved ActivityRecords between yesterday and now, + /// with the DT_CONTINUOUS_STEPS_DELTA data type + void getActivityRecord() async { + log( + 'getActivityRecord', + _activityTextController, + LogOptions.call, + ); + // Create start time that will be used to read activity record. + DateTime startTime = DateTime.now().subtract(const Duration(days: 1)); + + // Create end time that will be used to read activity record. + DateTime endTime = DateTime.now().add(const Duration(hours: 3)); + + ActivityRecordReadOptions activityRecordReadOptions = ActivityRecordReadOptions( + activityRecordId: "ActivityRecordId0", + activityRecordName: null, + startTime: startTime, + endTime: endTime, + timeUnit: TimeUnit.MILLISECONDS, + dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, + ); + try { + List result = await ActivityRecordsController.getActivityRecord( + activityRecordReadOptions, + ); + log( + 'getActivityRecord', + _activityTextController, + LogOptions.success, + result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', + ); + } on PlatformException catch (e) { + log( + 'getActivityRecord', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + /// Starts the ActivityRecord with the id:`ActivityRecordRun1` + void beginActivityRecord() async { + try { + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.call, + ); + // Build an ActivityRecord object + ActivityRecord activityRecord = ActivityRecord( + id: 'ActivityRecordRun0', + name: 'BeginActivityRecord', + description: 'This is ActivityRecord begin test!', + activityTypeId: HiHealthActivities.running, + startTime: DateTime.now().subtract(const Duration(hours: 1)), + ); + await ActivityRecordsController.beginActivityRecord( + activityRecord, + ); + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'beginActivityRecord', + _activityTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Stops the ActivityRecord with the id:`ActivityRecordRun1` + void endActivityRecord() async { + try { + log( + 'endActivityRecord', + _activityTextController, + LogOptions.call, + ); + final List result = await ActivityRecordsController.endActivityRecord( + 'ActivityRecordRun0', + ); + // Return the list of activity records that have stopped + log( + 'endActivityRecord', + _activityTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'endActivityRecord', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + /// Ends all the ongoing activity records. + /// + /// Result list will be null if there is no ongoing activity record. + void endAllActivityRecords() async { + try { + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.call, + ); + // Return the list of activity records that have stopped + List result = await ActivityRecordsController.endAllActivityRecords(); + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.success, + result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', + ); + } on PlatformException catch (e) { + log( + 'endAllActivityRecords', + _activityTextController, + LogOptions.error, + result: e.message, + ); + } + } + + // + // + // End of ActivityRecordsController Methods + + // DataController Methods + // + // + /// Initializes a DataController instance with a list of HiHealtOptions. + void initDataController() async { + if (!mounted) return; + log( + 'init', + _dataTextController, + LogOptions.call, + ); + try { + _dataController = await DataController.init(); + log( + 'init', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'init', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Clears all the data inserted by the app. + void clearAll() async { + log('clearAll', _dataTextController, LogOptions.call); + try { + await _dataController.clearAll(); + log('clearAll', _dataTextController, LogOptions.success); + } on PlatformException catch (e) { + log('clearAll', _dataTextController, LogOptions.error, error: e.message); + } + } + + /// Deletes DT_CONTINUOUS_STEPS_DELTA type data by the specified time range. + void delete() async { + log( + 'delete', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataStreamName: 'STEPS_DELTA', + ); + + // Build the time range for the deletion: start time and end time. + DeleteOptions deleteOptions = DeleteOptions( + dataCollectors: [dataCollector], + startTime: DateTime.parse('2020-10-10 08:00:00'), + endTime: DateTime.parse('2020-10-10 12:30:00'), + ); + + // Call the api with the constructed DeleteOptions instance. + try { + _dataController.delete(deleteOptions); + log( + 'delete', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'delete', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Inserts a sampling set with the DT_CONTINUOUS_STEPS_DELTA data type at the + /// specified start and end dates. + void insert() async { + log( + 'insert', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataStreamName: 'STEPS_DELTA', + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + // You can use sampleSets to add more sampling points to the sampling dataset. + SampleSet sampleSet = SampleSet( + dataCollector, + [ + SamplePoint( + dataCollector: dataCollector, + startTime: DateTime.parse('2020-10-10 12:00:00'), + endTime: DateTime.parse('2020-10-10 12:12:00'), + fieldValueOptions: FieldInt( + Field.FIELD_STEPS_DELTA, + 100, + ), + ), + ], + ); + // Call the api with the constructed sample set. + try { + _dataController.insert(sampleSet); + log( + 'insert', + _dataTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'insert', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // Reads the user data between the specified start and end dates. + void read() async { + log( + 'read', + _dataTextController, + LogOptions.call, + ); + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + dataStreamName: 'STEPS_DELTA', + ); + + // Build the time range for the query: start time and end time. + ReadOptions readOptions = ReadOptions( + dataCollectors: [ + dataCollector, + ], + startTime: DateTime.parse('2020-10-10 12:00:00'), + endTime: DateTime.parse('2020-10-10 12:12:00'), + )..groupByTime(10000); + + // Call the api with the constructed ReadOptions instance. + try { + ReadReply? readReply = await _dataController.read(readOptions); + log( + 'read', + _dataTextController, + LogOptions.success, + result: readReply.toString(), + ); + } on PlatformException catch (e) { + log( + 'read', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the daily summation between the dates: `2020.10.02` to `2020.12.15` for multiple data types. + /// Note that the time format is different for this method. + void readDailySummationList() async { + log( + 'readDailySummationList', + _dataTextController, + LogOptions.call, + ); + try { + List? sampleSets = await _dataController.readDailySummationList( + [DataType.DT_CONTINUOUS_STEPS_DELTA, DataType.DT_CONTINUOUS_CALORIES_BURNT], + 20201002, + 20201003, + ); + log( + 'readDailySummationList', + _dataTextController, + LogOptions.success, + result: sampleSets.toString(), + ); + } on PlatformException catch (e) { + log( + 'readDailySummationList', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the steps summation for today. + void readTodaySummation() async { + log( + 'readTodaySummation', + _dataTextController, + LogOptions.call, + ); + try { + SampleSet? sampleSet = await _dataController.readTodaySummation( + DataType.DT_CONTINUOUS_STEPS_DELTA, + ); + log( + 'readTodaySummation', + _dataTextController, + LogOptions.success, + result: sampleSet.toString(), + ); + } on PlatformException catch (e) { + log( + 'readTodaySummation', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Updates DT_CONTINUOUS_STEPS_DELTA for the specified dates. + void update() async { + log( + 'update', + _dataTextController, + LogOptions.call, + ); + + // Build the dataCollector object + DataCollector dataCollector = DataCollector( + dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, + dataStreamName: 'STEPS_DELTA', + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + // You can use sampleSets to add more sampling points to the sampling dataset. + SampleSet sampleSet = SampleSet( + dataCollector, + [ + SamplePoint( + dataCollector: dataCollector, + startTime: DateTime.parse('2020-12-12 09:00:00'), + endTime: DateTime.parse('2020-12-12 09:05:00'), + fieldValueOptions: FieldInt( + Field.FIELD_STEPS_DELTA, + 120, + ), + ), + ], + ); + + // Build a parameter object for the update. + // Note: (1) The start time of the modified object updateOptions can not be greater than the minimum + // value of the start time of all sample data points in the modified data sample set + // (2) The end time of the modified object updateOptions can not be less than the maximum value of the + // end time of all sample data points in the modified data sample set + UpdateOptions updateOptions = UpdateOptions( + startTime: DateTime.parse('2020-12-12 08:00:00'), + endTime: DateTime.parse('2020-12-12 09:25:00'), + sampleSet: sampleSet, + ); + try { + await _dataController.update(updateOptions); + log( + 'update', + _dataTextController, + LogOptions.success, + result: sampleSet.toString(), + ); + } on PlatformException catch (e) { + log( + 'update', + _dataTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of DataController Methods + + // SettingController Methods + // + /// Adds a custom DataType with the FIELD_ALTITUDE. + void addDataType() async { + log( + 'addDataType', + _settingTextController, + LogOptions.call, + ); + try { + // The name of the created data type must be prefixed with the package name + // of the app. Otherwise, the creation fails. If the same data type is tried to + // be added again an exception will be thrown. + DataTypeAddOptions options = DataTypeAddOptions( + '$packageName.myCustomDataType', + [ + const Field.newIntField('myIntField'), + Field.FIELD_ALTITUDE, + ], + ); + final DataType dataTypeResult = await SettingController.addDataType( + options, + ); + log( + 'addDataType', + _settingTextController, + LogOptions.success, + result: dataTypeResult.toString(), + ); + } on PlatformException catch (e) { + log( + 'addDataType', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Reads the inserted data type on the [addDataType] method. + void readDataType() async { + log( + 'readDataType', + _settingTextController, + LogOptions.call, + ); + try { + final DataType dataTypeResult = await SettingController.readDataType( + '$packageName.myCustomDataType', + ); + log( + 'readDataType', + _settingTextController, + LogOptions.success, + result: dataTypeResult.toString(), + ); + } on PlatformException catch (e) { + log( + 'readDataType', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Disables the Health Kit function, cancels user authorization, and cancels + /// all data records. (The task takes effect in 24 hours.) + void disableHiHealth() async { + log( + 'disableHiHealth', + _settingTextController, + LogOptions.call, + ); + try { + await SettingController.disableHiHealth(); + log( + 'disableHiHealth', + _settingTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'disableHiHealth', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Checks the user privacy authorization to Health Kit. Redirects the user to + /// the Authorization screen if the permissions are not given. + void checkHealthAppAuthorization() async { + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.call, + ); + try { + await SettingController.checkHealthAppAuthorization(); + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'checkHealthAppAuthorization', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Checks the user privacy authorization to Health Kit. If authorized `true` + /// value would be returned. + void getHealthAppAuthorization() async { + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.call, + ); + try { + final bool result = await SettingController.getHealthAppAuthorization(); + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'getHealthAppAuthorization', + _settingTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void requestAuth() async { + final HealthKitAuthResult res = await SettingController.requestAuthorizationIntent( + [ + Scope.HEALTHKIT_STEP_READ, + Scope.HEALTHKIT_STEP_WRITE, + Scope.HEALTHKIT_HEIGHTWEIGHT_READ, + Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, + Scope.HEALTHKIT_HEARTRATE_READ, + Scope.HEALTHKIT_HEARTRATE_WRITE, + Scope.HEALTHKIT_ACTIVITY_RECORD_READ, + Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, + Scope.HEALTHKIT_HEARTHEALTH_READ, + Scope.HEALTHKIT_HEARTHEALTH_WRITE, + ], + true, + ); + debugPrint(res.authAccount?.accessToken); + } + + // + // + // End of SettingController Methods + + // AutoRecorderController Methods + // + // + // Callback function for AutoRecorderStream event. + void _onAutoRecorderEvent(SamplePoint? res) { + log( + '[AutoRecorderEvent] obtained, SamplePoint Field Value is ${res?.fieldValues?.toString()}', + _autoRecorderTextController, + LogOptions.custom, + ); + } + + /// Starts an Android Foreground Service to count the steps of the user. + /// The steps will be emitted to the AutoRecorderStream. + void startRecord() async { + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.call, + ); + try { + await AutoRecorderController.startRecord( + DataType.DT_CONTINUOUS_STEPS_TOTAL, + NotificationProperties( + title: 'HMS Flutter Health Demo', + text: 'Counting steps', + subText: 'this is a subtext', + ticker: 'this is a ticker', + showChronometer: true, + ), + ); + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'startRecord', + _autoRecorderTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Ends the Foreground service and stops the step count events. + void stopRecord() async { + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.call, + ); + try { + await AutoRecorderController.stopRecord( + DataType.DT_CONTINUOUS_STEPS_TOTAL, + ); + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'endRecord', + _autoRecorderTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of AutoRecorderController Methods + + // ConsentController Methods + // + /// Obtains the application id from the agconnect-services.json file. + void getAppId() async { + log( + 'getAppId', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + log( + 'getAppId', + _consentTextController, + LogOptions.success, + result: appId, + ); + } on PlatformException catch (e) { + log( + 'getAppId', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Gets the granted permission scopes for the app. + void getScopes() async { + log( + 'getScopes', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + final ScopeLangItem scopeLangItem = await ConsentsController.getScopes( + 'en-gb', + appId, + ); + log( + 'getScopes', + _consentTextController, + LogOptions.success, + result: scopeLangItem.toString(), + ); + } on PlatformException catch (e) { + log( + 'getScopes', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Revokes all the permissions that authorized for this app. + void revoke() async { + log( + 'revoke', + _consentTextController, + LogOptions.call, + ); + try { + final String appId = await ConsentsController.getAppId(); + await ConsentsController.revoke(appId); + log( + 'revoke', + _consentTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'revoke', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + /// Revokes the distance read/write permissions for the app. + void revokeWithScopes() async { + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.call, + ); + try { + // Obtain the application id. + final String appId = await ConsentsController.getAppId(); + // Call the revokeWithScopes method with desired scopes. + await ConsentsController.revokeWithScopes( + appId, + [ + Scope.HEALTHKIT_DISTANCE_WRITE, + Scope.HEALTHKIT_DISTANCE_READ, + ], + ); + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'revokeWithScopes', + _consentTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of ConsentController Methods + + // HealthController Methods + // + void addHealthRecord() async { + log( + 'addHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2023, 5, 11); + final DateTime endTime = DateTime(2023, 5, 13); + + DataCollector contDataCollector = DataCollector( + dataStreamName: 'contDataCollector', + packageName: packageName, + dataType: DataType.POLYMERIZE_CONTINUOUS_HEART_RATE_STATISTICS, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + DataCollector instDataCollector = DataCollector( + dataStreamName: 'instDataCollector', + packageName: packageName, + dataType: DataType.DT_INSTANTANEOUS_HEART_RATE, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ); + + List subDataDetails = [ + SampleSet(instDataCollector, [ + SamplePoint( + dataCollector: instDataCollector, + ) + ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + ..setFieldValue(Field.FIELD_BPM, 88.0) + ]) + ]; + + List subDataSummary = [ + SamplePoint( + dataCollector: contDataCollector, + ) + ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + ..setFieldValue(Field.FIELD_AVG, 90.0) + ..setFieldValue(Field.FIELD_MAX, 100.0) + ..setFieldValue(Field.FIELD_MIN, 80.0) + ..setFieldValue(Field.LAST, 85.0) + ]; + + final HealthRecord healthRecord = HealthRecord( + startTime: startTime, + endTime: endTime, + metadata: 'Data', + dataCollector: DataCollector( + dataStreamName: 'such as step count', + packageName: packageName, + dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ), + ) + ..setSubDataSummary(subDataSummary) + ..setSubDataDetails(subDataDetails) + ..setFieldValue(HealthFields.FIELD_THRESHOLD, 42.0) + ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 48.0) + ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 42.0) + ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.0); + + final String? result = await HealthRecordController.addHealthRecord( + HealthRecordInsertOptions( + healthRecord: healthRecord, + ), + ); + log( + 'addHealthRecord', + _healthTextController, + LogOptions.success, + result: result.toString(), + ); + } on PlatformException catch (e) { + log( + 'addHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void getHealthRecord() async { + log( + 'getHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2023, 5, 11); + final DateTime endTime = DateTime(2023, 5, 13); + + HealthRecordReply result = await HealthRecordController.getHealthRecord( + HealthRecordReadOptions( + packageName: packageName, + ) + ..setSubDataTypeList( + [ + DataType.DT_INSTANTANEOUS_HEART_RATE, + ], + ) + ..setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS, + ) + ..readByDataType( + HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + ) + ..readHealthRecordsFromAllApps(), + ); + log( + 'getHealthRecord', + _healthTextController, + LogOptions.success, + result: result.healthRecords[0].toJson(), + ); + } on PlatformException catch (e) { + log( + 'getHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void updateHealthRecord() async { + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + final DateTime startTime = DateTime(2022, 10, 11); + final DateTime endTime = DateTime(2022, 10, 12); + final HealthRecord healthRecord = HealthRecord( + startTime: startTime, + endTime: endTime, + metadata: 'Data', + dataCollector: DataCollector( + dataStreamName: 'such as step count', + packageName: packageName, + dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, + dataGenerateType: DataGenerateType.DATA_TYPE_RAW, + ), + ) + ..setFieldValue(HealthFields.FIELD_THRESHOLD, 41.9) + ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 49.1) + ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 41.1) + ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.1); + await HealthRecordController.updateHealthRecord( + HealthRecordUpdateOptions( + healthRecord: healthRecord, + healthRecordId: '', + ), + ); + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'updateHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + void deleteHealthRecord() async { + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.call, + ); + try { + await HealthRecordController.deleteHealthRecord( + HealthRecordDeleteOptions( + startTime: DateTime.now().subtract(const Duration(days: 14)), + endTime: DateTime.now(), + )..setHealthRecordIds( + [ + '', + ], + ), + ); + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.success, + ); + } on PlatformException catch (e) { + log( + 'deleteHealthRecord', + _healthTextController, + LogOptions.error, + error: e.message, + ); + } + } + + // + // + // End of HealthController Methods + + // App's widgets. + // + // + Widget expansionCard({ + required String titleText, + required List children, + }) { + return Card( + margin: componentPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: ExpansionTile( + title: Text( + titleText, + style: cardTitleTextStyle, + ), + children: children, + ), + ); + } + + Widget loggingArea( + TextEditingController moduleTextController, + ) { + return Column( + children: [ + Container( + margin: componentPadding, + padding: const EdgeInsets.all(8.0), + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + border: Border.all(color: Colors.black12), + ), + child: TextField( + readOnly: true, + maxLines: 15, + controller: moduleTextController, + decoration: const InputDecoration( + enabledBorder: InputBorder.none, + ), + ), + ), + TextButton( + child: const Text('Clear Log'), + onPressed: () => setState(() { + moduleTextController.text = ''; + }), + ) + ], + ); + } + + void showSnackBar( + String text, { + Color color = Colors.blue, + }) { + final SnackBar snackBar = SnackBar( + content: Text(text), + backgroundColor: color, + action: SnackBarAction( + label: 'Close', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + }, + ), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: const Text( + 'Huawei Health Kit', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + elevation: 0.0, + actions: [ + IconButton( + onPressed: requestAuth, + icon: const Icon(Icons.ac_unit), + ), + ], + ), + body: Builder( + builder: (BuildContext context) { + return ListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + children: [ + // Sign In Widgets + Card( + margin: componentPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: componentPadding, + child: Text( + 'Tap to SignIn button to obtain the HMS Account to complete ' + 'login and authorization, and then use other buttons ' + 'to try the related API functions.', + textAlign: TextAlign.center, + ), + ), + const Padding( + padding: componentPadding, + child: Text( + 'Note: If the login page is not displayed, change the package ' + 'name, AppID, and configure the signature file by referring ' + 'to the developer guide on the official website.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.blue, + ), + ), + ), + Container( + padding: componentPadding, + width: double.infinity, + child: OutlinedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Colors.blue, + ), + ), + child: const Text( + 'SignIn', + style: TextStyle( + color: Colors.white, + ), + ), + onPressed: () => signIn(), + ), + ), + ], + ), + ), + + // ActivityRecordsController + expansionCard( + titleText: 'ActivityRecords Controller', + children: [ + loggingArea(_activityTextController), + ListTile( + title: const Text('AddActivityRecord'), + onTap: () => addActivityRecord(), + ), + ListTile( + title: const Text('GetActivityRecord'), + onTap: () => getActivityRecord(), + ), + ListTile( + title: const Text('beginActivityRecord'), + onTap: () => beginActivityRecord(), + ), + ListTile( + title: const Text('endActivityRecord'), + onTap: () => endActivityRecord(), + ), + ListTile( + title: const Text('endAllActivityRecords'), + onTap: () => endAllActivityRecords(), + ), + ], + ), + // DataController Widgets + expansionCard( + titleText: 'DataController', + children: [ + loggingArea(_dataTextController), + ListTile( + title: const Text('readTodaySummation'), + onTap: () => readTodaySummation(), + ), + ListTile( + title: const Text('readDailySummationList'), + onTap: () => readDailySummationList(), + ), + ListTile( + title: const Text('insert'), + onTap: () => insert(), + ), + ListTile( + title: const Text('read'), + onTap: () => read(), + ), + ListTile( + title: const Text('update'), + onTap: () => update(), + ), + ListTile( + title: const Text('delete'), + onTap: () => delete(), + ), + ListTile( + title: const Text('clearAll'), + onTap: () => clearAll(), + ), + ], + ), + // SettingController Widgets. + expansionCard( + titleText: 'SettingController', + children: [ + loggingArea(_settingTextController), + ListTile( + title: const Text('addDataType'), + onTap: () => addDataType(), + ), + ListTile( + title: const Text('readDataType'), + onTap: () => readDataType(), + ), + ListTile( + title: const Text('disableHiHealth'), + onTap: () => disableHiHealth(), + ), + ListTile( + title: const Text('checkHealthAppAuthorization'), + onTap: () => checkHealthAppAuthorization(), + ), + ListTile( + title: const Text('getHealthAppAuthorization'), + onTap: () => getHealthAppAuthorization(), + ), + ], + ), + // AutoRecorderController Widgets + expansionCard( + titleText: 'AutoRecorderController', + children: [ + loggingArea(_autoRecorderTextController), + ListTile( + title: const Text('startRecord'), + onTap: () => startRecord(), + ), + ListTile( + title: const Text('stopRecord'), + onTap: () => stopRecord(), + ), + ], + ), + // Consent Controller Widgets + expansionCard( + titleText: 'ConsentController', + children: [ + loggingArea(_consentTextController), + ListTile( + title: const Text('getAppId'), + onTap: () => getAppId(), + ), + ListTile( + title: const Text('getScopes'), + onTap: () => getScopes(), + ), + ListTile( + title: const Text('revoke'), + onTap: () => revoke(), + ), + ListTile( + title: const Text('revokeWithScopes'), + onTap: () => revokeWithScopes(), + ), + ], + ), + + // Health Controller Widgets + expansionCard( + titleText: 'HealthController', + children: [ + loggingArea(_healthTextController), + ListTile( + title: const Text('addHealthRecord'), + onTap: () => addHealthRecord(), + ), + ListTile( + title: const Text('getHealthRecord'), + onTap: () => getHealthRecord(), + ), + ListTile( + title: const Text('updateHealthRecord'), + onTap: () => updateHealthRecord(), + ), + ListTile( + title: const Text('deleteHealthRecord'), + onTap: () => deleteHealthRecord(), + ), + ], + ), + ], + ); + }, + ), + ); + } +} + +/// Options for logging. +enum LogOptions { + call, + success, + error, + custom, +} diff --git a/lib/presentation/smartwatches/smartwatch_instructions_page.dart b/lib/presentation/smartwatches/smartwatch_instructions_page.dart new file mode 100644 index 0000000..7f17f5a --- /dev/null +++ b/lib/presentation/smartwatches/smartwatch_instructions_page.dart @@ -0,0 +1,466 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/health_dashboard/health_dashboard.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SmartwatchInstructionsPage extends StatelessWidget { + const SmartwatchInstructionsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: LocaleKeys.smartWatches.tr(), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: SingleChildScrollView( + child: Platform.isIOS ? getIOSInstructionsUI(context) : getAndroidInstructionsUI(context), + ), + ), + ), + ); + } + + Widget getAndroidInstructionsUI(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Supported Smart Watches".needTranslation.toText20(isBold: true), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_ultra.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch Ultra", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 8 Classic", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_8.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 8", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_7_classic.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 7 Classic", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_watch_7.webp", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Watch 7", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/galaxy_fit_3.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Galaxy Fit3", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + SizedBox(height: 12), + "Please make sure that your Samsung Watch is connected to your Phone, is actively synced & updated.".needTranslation.toText14(isBold: true), + SizedBox(height: 12), + "Before syncing data, please make sure that you have followed the instructions properly.".needTranslation.toText14(isBold: true), + SizedBox(height: 12), + InkWell( + onTap: () { + showInstructionsDialog(context); + }, + child: "View watch instructions".needTranslation.toText12(isBold: true, color: AppColors.textColor, isUnderLine: true)), + SizedBox( + height: 130.h, + ), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + await Health().getHealthConnectSdkStatus().then((val) { + print('Health Connect SDK Status: $val'); + if (val == HealthConnectSdkStatus.sdkAvailable) { + Navigator.of(context).push( + CustomPageRoute( + page: HealthDashboard(), + ), + ); + } else { + getIt.get().showErrorBottomSheet( + message: "Seems like you do not have Health Connect App installed. Please install it from the Play Store to sync your health data.".needTranslation, + onOkPressed: () { + Navigator.pop(context); + Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata"); + launchUrl(uri, mode: LaunchMode.externalApplication); + }); + return; + } + }); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + // DefaultButton( + // LocaleKeys.confirm.tr(context: context), + // () async { + // await Health().getHealthConnectSdkStatus().then((val) { + // print('Health Connect SDK Status: $val'); + // if (val == HealthConnectSdkStatus.sdkAvailable) { + // Navigator.of(context).push( + // FadePage( + // page: HealthDashboard(), // Replace with the actual vital signs page + // // page: HealthApp(), // Replace with the actual vital signs page + // ), + // ); + // } else { + // Utils.showAppDialog(context, LocaleKeys.error.tr(context: context), LocaleKeys.healthConnectNotInstalled.tr(), () { + // Navigator.pop(context); + // Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata"); + // launchUrl(uri, mode: LaunchMode.externalApplication); + // }); + // return; + // } + // }); + // }, + // ) + ], + ); + } + + Widget getIOSInstructionsUI(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Supported Smart Watches".needTranslation.toText20(isBold: true), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-1.jpeg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 5", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-2.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 6", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-3.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 7", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-4.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 8", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/apple-watch-5.jpg", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 9", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 15.0), + child: Row( + children: [ + Image.asset("assets/images/png/smartwatches/Apple-Watch-6.png", width: 70.0, height: 70.0), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.21, + child: Text("Apple Watch Series 10", overflow: TextOverflow.clip, style: TextStyle(fontSize: 12.0)), + ), + ) + ], + ), + ), + ), + ], + ), + ), + SizedBox(height: 12), + "Please make sure that your Apple Watch is connected to your iPhone, is actively synced & updated.".toText14(isBold: true), + SizedBox(height: 12), + "Before syncing data, please make sure that you have followed the instructions properly.".toText14(isBold: true), + SizedBox(height: 12), + InkWell( + onTap: () { + showInstructionsDialog(context); + }, + child: "View watch instructions".toText12(isBold: true, color: AppColors.textColor, isUnderLine: true), + ), + SizedBox( + height: 130.h, + ), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + Navigator.of(context).push( + CustomPageRoute( + page: HealthDashboard(), + ), + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ), + ], + ); + } + + showInstructionsDialog(BuildContext context) { + showGeneralDialog( + barrierColor: Colors.black.withOpacity(0.5), + transitionBuilder: (context, a1, a2, widget) { + final curvedValue = Curves.easeInOutBack.transform(a1.value) - 1.0; + return Transform( + transform: Matrix4.translationValues(0.0, curvedValue * 200, 0.0), + child: Opacity( + opacity: a1.value, + child: Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 350.0, + padding: EdgeInsets.all(21), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "SmartWatch Tracker".toText18(), + IconButton( + icon: Icon( + Icons.close, + color: Color(0xff2E303A), + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (Platform.isIOS + ? "Make sure that you have installed 'Health' App & 'Watch' App from Apple Store." + : "Please make sure that your Samsung Watch is connected to your Phone, is actively synced & updated.") + .needTranslation + .toText14(), + SizedBox(height: 12), + ], + ) + ], + ), + ), + ], + ), + ), + ), + ); + }, + transitionDuration: Duration(milliseconds: 500), + barrierDismissible: true, + barrierLabel: '', + context: context, + pageBuilder: (context, animation1, animation2) { + return SizedBox(); + //Chanbged By Aamir + }); + } +} diff --git a/lib/presentation/smartwatches/widgets/health_chart.dart b/lib/presentation/smartwatches/widgets/health_chart.dart new file mode 100644 index 0000000..fe6ae78 --- /dev/null +++ b/lib/presentation/smartwatches/widgets/health_chart.dart @@ -0,0 +1,87 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class HealthChart extends StatelessWidget { + final List spots; + final String title; + final Color gradientColor; + + const HealthChart({ + Key? key, + required this.spots, + required this.title, + required this.gradientColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 200, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Expanded( + child: LineChart( + LineChartData( + gridData: FlGridData(show: false), + titlesData: FlTitlesData(show: false), + borderData: FlBorderData(show: false), + minX: 0, + maxX: spots.length.toDouble() - 1, + minY: spots.map((e) => e.y).reduce((a, b) => a < b ? a : b), + maxY: spots.map((e) => e.y).reduce((a, b) => a > b ? a : b), + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + gradient: LinearGradient( + colors: [ + gradientColor.withOpacity(0.5), + gradientColor, + ], + ), + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + gradientColor.withOpacity(0.1), + gradientColor.withOpacity(0.2), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/smartwatches/widgets/health_metric.dart b/lib/presentation/smartwatches/widgets/health_metric.dart new file mode 100644 index 0000000..5375966 --- /dev/null +++ b/lib/presentation/smartwatches/widgets/health_metric.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HealthMetricInfo { + final HealthDataType type; + final String nameEn; + final String nameAr; + final String unit; + final Color color; + final IconData icon; + final String description; + final double minHealthyValue; + final double maxHealthyValue; + final String svgIcon; + + const HealthMetricInfo( + {required this.type, + required this.nameEn, + required this.nameAr, + required this.unit, + required this.color, + required this.icon, + required this.description, + required this.minHealthyValue, + required this.maxHealthyValue, + required this.svgIcon}); +} + +class HealthMetrics { + static final metrics = [ + HealthMetricInfo( + type: HealthDataType.HEART_RATE, + nameEn: "Heart Rate", + nameAr: "معدل النبض", + unit: 'BPM', + color: AppColors.primaryRedColor, + icon: Icons.favorite, + description: "Your heart rate indicates how many times your heart beats per minute".needTranslation, + minHealthyValue: 60, + maxHealthyValue: 100, + svgIcon: "assets/images/smartwatches/heartrate_icon.svg"), + HealthMetricInfo( + type: HealthDataType.BLOOD_OXYGEN, + nameEn: "Blood Oxygen", + nameAr: "أكسجين الدم", + unit: '%', + // color: Colors.blue, + color: Color(0xff3A3558), + icon: Icons.air, + description: "Blood oxygen level indicates how much oxygen your red blood cells are carrying".needTranslation, + minHealthyValue: 95, + maxHealthyValue: 100, + svgIcon: "assets/images/smartwatches/bloodoxygen_icon.svg"), + HealthMetricInfo( + type: HealthDataType.STEPS, + nameEn: "Steps", + nameAr: "خطوات", + unit: 'steps', + // color: Colors.green, + color: Color(0xff3263B8), + icon: Icons.directions_walk, + description: "Number of steps taken throughout the day".needTranslation, + minHealthyValue: 7000, + maxHealthyValue: 15000, + svgIcon: "assets/images/smartwatches/steps_icon.svg"), + HealthMetricInfo( + type: Platform.isIOS ? HealthDataType.ACTIVE_ENERGY_BURNED : HealthDataType.TOTAL_CALORIES_BURNED, + nameEn: "Active Calories", + nameAr: "السعرات الحرارية النشطة", + unit: 'kcal', + color: Color(0xffD59E95), + icon: Icons.local_fire_department, + description: "Calories burned during physical activity".needTranslation, + minHealthyValue: 300, + maxHealthyValue: 1000, + svgIcon: "assets/images/smartwatches/calories_icon.svg"), + HealthMetricInfo( + type: Platform.isIOS ? HealthDataType.DISTANCE_WALKING_RUNNING : HealthDataType.DISTANCE_DELTA, + nameEn: "Distance Covered", + nameAr: "المسافة المغطاة", + unit: 'KMs', + // color: mainPurple, + color: Color(0xff6A46F5), + icon: Icons.directions_run, + description: "Distance covered throughout the day".needTranslation, + minHealthyValue: 3, + maxHealthyValue: 10, + svgIcon: "assets/images/smartwatches/distance_icon.svg"), + // Add more metrics as needed + ]; +} diff --git a/lib/presentation/smartwatches/widgets/health_metric_card.dart b/lib/presentation/smartwatches/widgets/health_metric_card.dart new file mode 100644 index 0000000..023f41b --- /dev/null +++ b/lib/presentation/smartwatches/widgets/health_metric_card.dart @@ -0,0 +1,151 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:health/health.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/health_metric.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HealthMetricCard extends StatelessWidget { + final HealthMetricInfo metric; + final List data; + final VoidCallback onTap; + + const HealthMetricCard({ + Key? key, + required this.metric, + required this.data, + required this.onTap, + }) : super(key: key); + + // Helper method to convert HealthValue to double + double _getValueAsDouble(HealthValue value, HealthDataType type) { + if (value is NumericHealthValue) { + return value.numericValue.toDouble(); + } + return 0.0; + } + + @override + Widget build(BuildContext context) { + final latestValue = data.isNotEmpty ? _getValueAsDouble(data.last.value, data.last.type) : 0.0; + final isHealthy = latestValue >= metric.minHealthyValue && latestValue <= metric.maxHealthyValue; + + return Card( + color: AppColors.whiteColor, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + metric.svgIcon, + height: 30.h, + width: 30.h, + fit: BoxFit.contain, + // color: color, + ), + // Icon( + // metric.icon, + // color: metric.color, + // size: 30, + // ), + if (data.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isHealthy ? Colors.green[100] : Color(0xffD7D2F3), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + isHealthy ? "Healthy".needTranslation : 'Warning'.needTranslation, + style: TextStyle( + color: isHealthy ? Colors.green[700] : AppColors.blackColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + (getIt.get().isArabic() ? metric.nameAr : metric.nameEn).toText18(isBold: true, maxlines: 1), + // Text( + // metric.name, + // style: const TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.bold, + // ), + // ), + const SizedBox(height: 8), + Text( + data.isEmpty ? LocaleKeys.noDataAvailable.tr(context: context) : '${latestValue.toStringAsFixed(metric.type == HealthDataType.STEPS ? 0 : 1)} ${metric.unit}', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: metric.color, + ), + ), + if (data.isNotEmpty) ...[ + const SizedBox(height: 16), + SizedBox( + height: 100, + child: LineChart( + LineChartData( + gridData: FlGridData(show: false), + titlesData: FlTitlesData(show: false), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: _convertToSpots(data), + isCurved: true, + color: metric.color, + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: metric.color.withOpacity(0.1), + ), + ), + ], + ), + ), + ), + ], + ], + ), + ), + ), + ); + } + + List _convertToSpots(List data) { + if (data.isEmpty) return []; + + return List.generate( + data.length, + (index) => FlSpot( + index.toDouble(), + _getValueAsDouble(data[index].value, data[index].type), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index d5dc32c..1786dec 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -10,9 +10,11 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; class OrganSelectorPage extends StatefulWidget { @@ -24,25 +26,38 @@ class OrganSelectorPage extends StatefulWidget { class _OrganSelectorPageState extends State { late final AppState _appState; + late final DialogService dialogService; @override void initState() { super.initState(); _appState = getIt.get(); + dialogService = getIt(); } - void _onNextPressed(SymptomsCheckerViewModel viewModel) { + void _onNextPressed(SymptomsCheckerViewModel viewModel) async { if (!viewModel.validateSelection()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one organ'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one organ'.needTranslation, ); return; } + LoaderBottomSheet.showLoader(loadingText: "Please wait".needTranslation); + + final String userName = 'guest_user'; + final String password = '123456'; - context.navigateWithName(AppRoutes.symptomsSelectorScreen); + await viewModel.getSymptomsUserDetails( + userName: userName, + password: password, + onSuccess: () { + LoaderBottomSheet.hideLoader(); + context.navigateWithName(AppRoutes.symptomsSelectorPage); + }, + onError: (String error) { + LoaderBottomSheet.hideLoader(); + }, + ); } @override @@ -267,14 +282,15 @@ class _OrganSelectorPageState extends State { runSpacing: 8.h, children: viewModel.selectedOrgans.map((organ) { return AppCustomChipWidget( - labelText: organ.description, - backgroundColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - deleteIcon: AppAssets.cancel, - deleteIconColor: AppColors.primaryRedColor, - deleteIconHasColor: false, - onDeleteTap: () => viewModel.removeOrgan(organ.id), - ); + labelText: organ.description, + backgroundColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + deleteIcon: AppAssets.cancel, + deleteIconColor: AppColors.primaryRedColor, + deleteIconHasColor: false, + onDeleteTap: () { + viewModel.removeOrgan(organ.id); + }); }).toList(), ), ), diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 2c99515..a63d1e2 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/models/conditions_ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; @@ -18,8 +19,8 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; -class PossibleConditionsScreen extends StatelessWidget { - const PossibleConditionsScreen({super.key}); +class PossibleConditionsPage extends StatelessWidget { + const PossibleConditionsPage({super.key}); Widget _buildLoadingShimmer() { return ListView.separated( @@ -44,7 +45,7 @@ class PossibleConditionsScreen extends StatelessWidget { ); } - Widget _buildPredictionsList(List conditions) { + Widget _buildPredictionsList(BuildContext context, List conditions) { if (conditions.isEmpty) { return Center( child: Padding( @@ -60,6 +61,8 @@ class PossibleConditionsScreen extends StatelessWidget { ); } + final dialogService = getIt(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -82,11 +85,8 @@ class PossibleConditionsScreen extends StatelessWidget { description: conditionModel.description, possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum, onActionPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('We are not available for a week. May you Rest In Peace :('), - backgroundColor: AppColors.primaryRedColor, - ), + dialogService.showErrorBottomSheet( + message: 'We are not available for a week. May you Rest In Peace :(', ); }, ); @@ -168,7 +168,7 @@ class PossibleConditionsScreen extends StatelessWidget { if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) { return _buildLoadingShimmer(); } - return _buildPredictionsList(dummyConditions); + return _buildPredictionsList(context, dummyConditions); }, ), ), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 2992593..8669c3c 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -1,16 +1,17 @@ -import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; class RiskFactorsScreen extends StatefulWidget { @@ -21,28 +22,29 @@ class RiskFactorsScreen extends StatefulWidget { } class _RiskFactorsScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); - // Initialize symptom groups based on selected organs + dialogService = getIt(); + // Fetch risk factors based on selected symptoms WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); - viewModel.initializeSymptomGroups(); + viewModel.fetchRiskFactors(); }); } - void _onOptionSelected(int optionIndex) {} + void _onRiskFactorSelected(SymptomsCheckerViewModel viewModel, String riskFactorId) { + viewModel.toggleRiskFactorSelection(riskFactorId); + } void _onNextPressed(SymptomsCheckerViewModel viewModel) { - if (viewModel.hasSelectedSymptoms) { - // Navigate to triage screen - context.navigateWithName(AppRoutes.suggestionsScreen); + if (viewModel.hasSelectedRiskFactors) { + context.navigateWithName(AppRoutes.suggestionsPage); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one risk before proceeding'.needTranslation, ); } } @@ -51,27 +53,13 @@ class _RiskFactorsScreenState extends State { context.pop(); } - _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { - return showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: "Are you sure you want to restart the organ selection?".needTranslation, - isShowActionButtons: true, - onCancelTap: () => Navigator.pop(context), - onConfirmTap: () => onConfirm(), - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } + Widget _buildRiskFactorItem(SymptomsCheckerViewModel viewModel, String riskFactorId, String optionText) { + final bool selected = viewModel.isRiskFactorSelected(riskFactorId); - Widget _buildOptionItem(int index, bool selected, String optionText) { return GestureDetector( - onTap: () => _onOptionSelected(index), + onTap: () => _onRiskFactorSelected(viewModel, riskFactorId), child: Container( - margin: EdgeInsets.only(bottom: 12.h), + margin: EdgeInsets.only(bottom: 16.h), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -83,56 +71,137 @@ class _RiskFactorsScreenState extends State { decoration: BoxDecoration( color: selected ? AppColors.primaryRedColor : Colors.transparent, borderRadius: BorderRadius.circular(5.r), - border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), ), child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), Expanded( - child: Text( - optionText, - style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500), - ), - ), + child: optionText.toText14( + color: riskFactorId == "not_applicable" ? AppColors.errorColor : AppColors.textColor, + weight: FontWeight.w500, + )), ], ), ), ); } - Widget buildFactorsList() { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (Widget child, Animation animation) { - final offsetAnimation = Tween( - begin: const Offset(1.0, 0.0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeInOut, - )); - - return SlideTransition( - position: offsetAnimation, - child: FadeTransition( - opacity: animation, - child: child, + Widget _buildRiskFactorsList(SymptomsCheckerViewModel viewModel) { + return Container( + key: ValueKey(viewModel.riskFactorsList.length), + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.only(top: 24.h, left: 16.w, right: 16.w, bottom: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...viewModel.riskFactorsList.map((factor) { + return _buildRiskFactorItem(viewModel, factor.id ?? '', factor.getDisplayName()); + }), + SizedBox(height: 12.w), + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: RichText( + text: TextSpan( + style: TextStyle( + height: 1.3, + fontSize: 13.f, + fontWeight: FontWeight.w500, + color: AppColors.greyInfoTextColor, + ), + children: [ + TextSpan( + text: "Above you see the most common risk factors. Although /diagnosis may return questions about risk factors, " + .needTranslation, + ), + TextSpan( + text: "read more".needTranslation, + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // handle tap - navigate or show bottom sheet + debugPrint('Read more tapped'); + // Example: Navigator.push(context, MaterialPageRoute(builder: (_) => RiskFactorsDetailScreen())); + }, + ), + ], + ), + ), + ) + ], ), - ); - }, - child: Container( - width: double.infinity, - margin: EdgeInsets.symmetric(horizontal: 24.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...List.generate(4, (index) { - return _buildOptionItem(index, false, "currentQuestion.options[index].text"); + ], + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(3, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), + ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Risk Factor', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); }), - ], - ), + ), + ], ), ); } @@ -147,68 +216,19 @@ class _RiskFactorsScreenState extends State { children: [ Expanded( child: CollapsingListView( - title: "Risks".needTranslation, - leadingCallback: () => _buildConfirmationBottomSheet( - context: context, - onConfirm: () => { - context.pop(), - context.pop(), - }), - child: _buildEmptyState(), - // child: viewModel.organSymptomsGroups.isEmpty - // ? _buildEmptyState() - // : Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // SizedBox(height: 16.h), - // ...viewModel.organSymptomsGroups.map((group) { - // return Padding( - // padding: EdgeInsets.only(bottom: 16.h), - // child: Container( - // width: double.infinity, - // margin: EdgeInsets.symmetric(horizontal: 24.w), - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - // padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Expanded( - // child: Text( - // 'Possible symptoms related to "${group.organName}"', - // style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), - // ), - // ), - // ], - // ), - // SizedBox(height: 24.h), - // Wrap( - // runSpacing: 12.h, - // spacing: 8.w, - // children: group.symptoms.map((symptom) { - // bool isSelected = viewModel.isSymptomSelected(group.organId, symptom.id); - // return GestureDetector( - // onTap: () => viewModel.toggleSymptomSelection(group.organId, symptom.id), - // child: CustomSelectableChip( - // label: symptom.name, - // selected: isSelected, - // activeColor: AppColors.primaryRedBorderColor, - // activeTextColor: AppColors.primaryRedBorderColor, - // inactiveBorderColor: AppColors.bottomNAVBorder, - // inactiveTextColor: AppColors.textColor, - // ), - // ); - // }).toList(), - // ), - // ], - // ), - // ), - // ); - // }), - // ], - // ), + title: "Risk Factors".needTranslation, + leadingCallback: () => context.pop(), + child: viewModel.isRiskFactorsLoading + ? _buildLoadingShimmer() + : viewModel.riskFactorsList.isEmpty + ? _buildEmptyState() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildRiskFactorsList(viewModel), + ], + ), ), ), _buildStickyBottomCard(context, viewModel), @@ -229,7 +249,7 @@ class _RiskFactorsScreenState extends State { Icon(Icons.info_outline, size: 64.h, color: AppColors.greyTextColor), SizedBox(height: 16.h), Text( - 'No organs selected'.needTranslation, + 'No risk factors found'.needTranslation, style: TextStyle( fontSize: 18.f, fontWeight: FontWeight.w600, @@ -238,7 +258,7 @@ class _RiskFactorsScreenState extends State { ), SizedBox(height: 8.h), Text( - 'Please go back and select organs first'.needTranslation, + 'Based on your selected symptoms, no additional risk factors were identified.'.needTranslation, textAlign: TextAlign.center, style: TextStyle( fontSize: 14.f, diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index 2832515..d0d5b2f 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -1,16 +1,16 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; class SuggestionsScreen extends StatefulWidget { @@ -21,28 +21,32 @@ class SuggestionsScreen extends StatefulWidget { } class _SuggestionsScreenState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); - viewModel.initializeSymptomGroups(); + viewModel.fetchSuggestions(); }); } void _onOptionSelected(int optionIndex) {} + void _onSuggestionSelected(SymptomsCheckerViewModel viewModel, String suggestionId) { + viewModel.toggleSuggestionsSelection(suggestionId); + } + void _onNextPressed(SymptomsCheckerViewModel viewModel) { - if (viewModel.hasSelectedSymptoms) { + if (viewModel.hasSelectedSuggestions) { // Navigate to triage screen - context.navigateWithName(AppRoutes.triageScreen); + context.navigateWithName(AppRoutes.triagePage); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one option before proceeding'.needTranslation, ); } } @@ -51,27 +55,13 @@ class _SuggestionsScreenState extends State { context.pop(); } - _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { - return showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: "Are you sure you want to restart the organ selection?".needTranslation, - isShowActionButtons: true, - onCancelTap: () => Navigator.pop(context), - onConfirmTap: () => onConfirm(), - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } + Widget _buildSuggestionItem(SymptomsCheckerViewModel viewModel, String suggestionId, String optionText) { + final bool selected = viewModel.isSuggestionsSelected(suggestionId); - Widget _buildOptionItem(int index, bool selected, String optionText) { return GestureDetector( - onTap: () => _onOptionSelected(index), + onTap: () => _onSuggestionSelected(viewModel, suggestionId), child: Container( - margin: EdgeInsets.only(bottom: 12.h), + margin: EdgeInsets.only(bottom: 16.h), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -83,56 +73,111 @@ class _SuggestionsScreenState extends State { decoration: BoxDecoration( color: selected ? AppColors.primaryRedColor : Colors.transparent, borderRadius: BorderRadius.circular(5.r), - border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), ), child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), Expanded( - child: Text( - optionText, - style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500), - ), - ), + child: optionText.toText14( + color: suggestionId == "not_applicable" ? AppColors.errorColor : AppColors.textColor, + weight: FontWeight.w500, + )), ], ), ), ); } - Widget buildFactorsList() { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (Widget child, Animation animation) { - final offsetAnimation = Tween( - begin: const Offset(1.0, 0.0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeInOut, - )); - - return SlideTransition( - position: offsetAnimation, - child: FadeTransition( - opacity: animation, - child: child, + Widget _buildSuggestionsList(SymptomsCheckerViewModel viewModel) { + return Container( + key: ValueKey(viewModel.suggestionsList.length), + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.only(top: 24.h, left: 16.w, right: 16.w, bottom: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...viewModel.suggestionsList.map((factor) { + return _buildSuggestionItem(viewModel, factor.id ?? '', factor.getDisplayName()); + }), + SizedBox(height: 12.w), + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: "This is a list of symptoms suggested by our AI, based on the information gathered so far during the interview".toText12( + color: AppColors.greyInfoTextColor, + ), + ) + ], ), - ); - }, - child: Container( - width: double.infinity, - margin: EdgeInsets.symmetric(horizontal: 24.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...List.generate(4, (index) { - return _buildOptionItem(index, false, "currentQuestion.options[index].text"); + ], + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(3, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), + ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Suggestion', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); }), - ], - ), + ), + ], ), ); } @@ -148,68 +193,18 @@ class _SuggestionsScreenState extends State { Expanded( child: CollapsingListView( title: "Suggestions".needTranslation, - leadingCallback: () => _buildConfirmationBottomSheet( - context: context, - onConfirm: () => { - context.pop(), - context.pop(), - }), - child: _buildEmptyState(), - - // child: viewModel.organSymptomsGroups.isEmpty - // ? _buildEmptyState() - // : Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // SizedBox(height: 16.h), - // ...viewModel.organSymptomsGroups.map((group) { - // return Padding( - // padding: EdgeInsets.only(bottom: 16.h), - // child: Container( - // width: double.infinity, - // margin: EdgeInsets.symmetric(horizontal: 24.w), - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - // padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Expanded( - // child: Text( - // 'Possible symptoms related to "${group.organName}"', - // style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), - // ), - // ), - // ], - // ), - // SizedBox(height: 24.h), - // Wrap( - // runSpacing: 12.h, - // spacing: 8.w, - // children: group.symptoms.map((symptom) { - // bool isSelected = viewModel.isSymptomSelected(group.organId, symptom.id); - // return GestureDetector( - // onTap: () => viewModel.toggleSymptomSelection(group.organId, symptom.id), - // child: CustomSelectableChip( - // label: symptom.name, - // selected: isSelected, - // activeColor: AppColors.primaryRedBorderColor, - // activeTextColor: AppColors.primaryRedBorderColor, - // inactiveBorderColor: AppColors.bottomNAVBorder, - // inactiveTextColor: AppColors.textColor, - // ), - // ); - // }).toList(), - // ), - // ], - // ), - // ), - // ); - // }), - // ], - // ), + leadingCallback: () => context.pop(), + child: viewModel.isSuggestionsLoading + ? _buildLoadingShimmer() + : viewModel.suggestionsList.isEmpty + ? _buildEmptyState() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildSuggestionsList(viewModel), + ], + ), ), ), _buildStickyBottomCard(context, viewModel), diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 522c5f8..d6036c6 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -8,6 +9,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -15,17 +17,20 @@ import 'package:hmg_patient_app_new/widgets/chip/custom_selectable_chip.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; -class SymptomsSelectorScreen extends StatefulWidget { - const SymptomsSelectorScreen({super.key}); +class SymptomsSelectorPage extends StatefulWidget { + const SymptomsSelectorPage({super.key}); @override - State createState() => _SymptomsSelectorScreenState(); + State createState() => _SymptomsSelectorPageState(); } -class _SymptomsSelectorScreenState extends State { +class _SymptomsSelectorPageState extends State { + late DialogService dialogService; + @override void initState() { super.initState(); + dialogService = getIt(); // Initialize symptom groups based on selected organs WidgetsBinding.instance.addPostFrameCallback((_) { final viewModel = context.read(); @@ -36,13 +41,10 @@ class _SymptomsSelectorScreenState extends State { void _onNextPressed(SymptomsCheckerViewModel viewModel) { if (viewModel.hasSelectedSymptoms) { // Navigate to triage screen - context.navigateWithName(AppRoutes.riskFactorsScreen); + context.navigateWithName(AppRoutes.riskFactorsPage); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select at least one symptom before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + dialogService.showErrorBottomSheet( + message: 'Please select at least one symptom before proceeding'.needTranslation, ); } } diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index aa0cd72..9d5d884 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -1,89 +1,213 @@ +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:hmg_patient_app_new/features/symptoms_checker/data/triage_questions_data.dart'; -import 'package:hmg_patient_app_new/features/symptoms_checker/models/triage_question_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:lottie/lottie.dart'; +import 'package:provider/provider.dart'; -class TriageScreen extends StatefulWidget { - const TriageScreen({super.key}); +class TriagePage extends StatefulWidget { + const TriagePage({super.key}); @override - State createState() => _TriageScreenState(); + State createState() => _TriagePageState(); } -class _TriageScreenState extends State { - late List triageQuestions; - int currentQuestionIndex = 0; +class _TriagePageState extends State { + late SymptomsCheckerViewModel viewModel; + late DialogService dialogService; @override void initState() { super.initState(); - triageQuestions = TriageQuestionsData.getSampleTriageQuestions(); + viewModel = context.read(); + dialogService = getIt.get(); + + // Start triage process when screen loads + WidgetsBinding.instance.addPostFrameCallback((_) { + _startTriage(); + }); + } + + void _startTriage() { + viewModel.startOrContinueTriage( + onSuccess: () { + _handleTriageResponse(); + }, + onError: (error) { + dialogService.showErrorBottomSheet( + message: error, + onOkPressed: () => context.pop(), + ); + }, + ); } - TriageQuestionModel get currentQuestion => triageQuestions[currentQuestionIndex]; + void _handleTriageResponse() { + // Case 1: Emergency evidence detected + if (viewModel.hasEmergencyEvidence) { + _showEmergencyDialog(); + return; + } - bool get isFirstQuestion => currentQuestionIndex == 0; + // Get the highest probability condition + final conditions = viewModel.currentConditions ?? []; + double highestProbability = 0.0; - bool get isLastQuestion => currentQuestionIndex == triageQuestions.length - 1; + if (conditions.isNotEmpty) { + final sortedConditions = List.from(conditions); + sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); + highestProbability = (sortedConditions.first.probability ?? 0.0) * 100; + } - void _onOptionSelected(int optionIndex) { - setState(() { - currentQuestion.selectOption(optionIndex); - }); + // Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered + if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 7) { + // Navigate to results/possible conditions screen + context.navigateWithName(AppRoutes.possibleConditionsPage); + return; + } + + // Continue triage - question is loaded, reset selection for new question + viewModel.resetTriageChoice(); + } + + void _showEmergencyDialog() { + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.primaryRedColor, + borderRadius: 24.h, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: AppColors.whiteColor, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + "Emergency".needTranslation.toText28(color: AppColors.whiteColor, isBold: true), + SizedBox(height: 8.h), + "Emergency evidence detected. Please seek medical attention." + .needTranslation + .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async => Navigator.of(context).pop(), + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.whiteColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), + ), + ), + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () {}, + ); + } + + bool get isFirstQuestion => viewModel.getTriageEvidence().isEmpty; + + void _onOptionSelectedForItem(String itemId, int choiceIndex) { + viewModel.selectTriageChoiceForItem(itemId, choiceIndex); } void _onPreviousPressed() { - if (!isFirstQuestion) { - setState(() { - currentQuestionIndex--; - }); - } + context.pop(); } void _onNextPressed() { - if (currentQuestion.isAnswered) { - currentQuestion.confirmSelection(); - if (isLastQuestion) { - context.navigateWithName(AppRoutes.possibleConditionsScreen); - } else { - setState(() { - currentQuestionIndex++; - }); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select an option before proceeding'.needTranslation), - backgroundColor: AppColors.errorColor, - ), + final currentQuestion = viewModel.currentTriageQuestion; + if (currentQuestion?.items == null || currentQuestion!.items!.isEmpty) { + dialogService.showErrorBottomSheet( + message: 'No question items available'.needTranslation, ); + return; } - } - _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { - return showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: "Are you sure you want to restart the organ selection?".needTranslation, - isShowActionButtons: true, - onCancelTap: () => Navigator.pop(context), - onConfirmTap: () => onConfirm(), - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, + // Check if all items have been answered + if (!viewModel.areAllTriageItemsAnswered) { + dialogService.showErrorBottomSheet(message: 'Please answer all questions before proceeding'.needTranslation); + return; + } + + // Collect all evidence from all items + for (var item in currentQuestion.items!) { + final itemId = item.id ?? ""; + if (itemId.isEmpty) continue; + + final selectedChoiceIndex = viewModel.getTriageChoiceForItem(itemId); + if (selectedChoiceIndex == null) continue; + + if (item.choices != null && selectedChoiceIndex < item.choices!.length) { + final selectedChoice = item.choices![selectedChoiceIndex]; + final choiceId = selectedChoice.id ?? ""; + + if (choiceId.isNotEmpty) { + viewModel.addTriageEvidence(itemId, choiceId); + } + } + } + + // Get all evidence: initial symptoms + risk factors + suggestions + triage evidence + List initialEvidenceIds = viewModel.getAllEvidenceIds(); + List> triageEvidence = viewModel.getTriageEvidence(); + + log("initialEvidenceIds: ${initialEvidenceIds.toString()}"); + log("triageEvidence: ${triageEvidence.toString()}"); + + // Call API with updated evidence + viewModel.getDiagnosisForTriage( + age: viewModel.selectedAge!, + sex: viewModel.selectedGender!.toLowerCase(), + evidenceIds: initialEvidenceIds, + triageEvidence: triageEvidence, + language: viewModel.appState.isArabic() ? 'ar' : 'en', + onSuccess: (response) { + _handleTriageResponse(); + }, + onError: (error) { + dialogService.showErrorBottomSheet(message: error); + }, ); } @@ -91,45 +215,137 @@ class _TriageScreenState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: "Triage".needTranslation, - // onLeadingTapped: () => _buildConfirmationBottomSheet( - // context: context, - // onConfirm: () => { - // context.pop(), - // context.pop(), - // }), - - leadingCallback: () => context.pop(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - _buildQuestionCard(), - ], + body: Consumer( + builder: (context, viewModel, child) { + // Show normal question UI + return Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Triage".needTranslation, + leadingCallback: () => _showConfirmationBeforeExit(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + _buildQuestionCard(viewModel), + ], + ), + ), ), + _buildStickyBottomCard(context, viewModel), + ], + ); + }, + ), + ); + } + + Widget _buildLoadingShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + // Create 2-3 shimmer cards + ...List.generate(1, (index) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: _buildShimmerCard(), + ); + }), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 24.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shimmer title + Container( + height: 40.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.r), ), + ).toShimmer2(isShow: true, radius: 24.r), + SizedBox(height: 16.h), + // Shimmer chips + Wrap( + runSpacing: 12.h, + spacing: 8.w, + children: List.generate(4, (index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all(color: AppColors.bottomNAVBorder, width: 1), + ), + child: Text( + 'Not Applicable Suggestion', + style: TextStyle(fontSize: 14.f, color: AppColors.textColor), + ), + ).toShimmer2(isShow: true, radius: 24.r); + }), ), - _buildStickyBottomCard(context), ], ), ); } - Widget _buildQuestionCard() { + void _showConfirmationBeforeExit(BuildContext context) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to exit? Your progress will be lost.".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () { + Navigator.pop(context); + context.pop(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + Widget _buildQuestionCard(SymptomsCheckerViewModel viewModel) { + if (viewModel.isTriageDiagnosisLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.currentTriageQuestion == null) { + return Center( + child: "No question available".needTranslation.toText16(weight: FontWeight.w500), + ); + } + + final question = viewModel.currentTriageQuestion; + if (question == null || question.items == null || question.items!.isEmpty) { + return SizedBox.shrink(); + } + return AnimatedSwitcher( duration: const Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { final offsetAnimation = Tween( begin: const Offset(1.0, 0.0), end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeInOut, - )); + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + ), + ); return SlideTransition( position: offsetAnimation, @@ -140,7 +356,7 @@ class _TriageScreenState extends State { ); }, child: Container( - key: ValueKey(currentQuestionIndex), + key: ValueKey(question.items!.first.id ?? viewModel.getTriageEvidence().length.toString()), width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 24.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -148,14 +364,36 @@ class _TriageScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - currentQuestion.question, - style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.w500, color: AppColors.textColor), - ), + // Main question text + (question.text ?? "").toText16(weight: FontWeight.w600, color: AppColors.textColor), SizedBox(height: 24.h), - ...List.generate(currentQuestion.options.length, (index) { - bool selected = currentQuestion.selectedOptionIndex == index; - return _buildOptionItem(index, selected, currentQuestion.options[index].text); + + // Show all items with dividers + ...List.generate(question.items!.length, (itemIndex) { + final item = question.items![itemIndex]; + final itemId = item.id ?? ""; + final choices = item.choices ?? []; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Item name (sub-question) + (item.name ?? "").toText14(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 8.h), + // Choices for this item + ...List.generate(choices.length, (choiceIndex) { + bool selected = viewModel.getTriageChoiceForItem(itemId) == choiceIndex; + return _buildOptionItem(itemId, choiceIndex, selected, choices[choiceIndex].label ?? ""); + }), + + // Add divider between items (but not after the last one) + if (itemIndex < question.items!.length - 1) ...[ + SizedBox(height: 8.h), + Divider(color: AppColors.bottomNAVBorder, thickness: 1), + SizedBox(height: 10.h), + ], + ], + ); }), ], ), @@ -163,9 +401,9 @@ class _TriageScreenState extends State { ); } - Widget _buildOptionItem(int index, bool selected, String optionText) { + Widget _buildOptionItem(String itemId, int choiceIndex, bool selected, String optionText) { return GestureDetector( - onTap: () => _onOptionSelected(index), + onTap: () => _onOptionSelectedForItem(itemId, choiceIndex), child: Container( margin: EdgeInsets.only(bottom: 12.h), child: Row( @@ -179,26 +417,34 @@ class _TriageScreenState extends State { decoration: BoxDecoration( color: selected ? AppColors.primaryRedColor : Colors.transparent, borderRadius: BorderRadius.circular(5.r), - border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.borderGrayColor, width: 1.w), + border: Border.all(color: selected ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor, width: 1.w), ), child: selected ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor) : null, ), SizedBox(width: 12.w), - Expanded( - child: Text( - optionText, - style: TextStyle(fontSize: 14.f, color: AppColors.textColor, fontWeight: FontWeight.w500), - ), - ), + Expanded(child: optionText.toText13(weight: FontWeight.w500)), ], ), ), ); } - Widget _buildStickyBottomCard(BuildContext context) { - final currentScore = TriageQuestionsData.calculateTotalScore(triageQuestions); - final suggestedCondition = TriageQuestionsData.getSuggestedCondition(currentScore); + Widget _buildStickyBottomCard(BuildContext context, SymptomsCheckerViewModel viewModel) { + // Get the top condition with highest probability + final conditions = viewModel.currentConditions ?? []; + String suggestedCondition = "Analyzing..."; + double probability = 0.0; + + if (conditions.isNotEmpty) { + // Sort by probability descending + final sortedConditions = List.from(conditions); + sortedConditions.sort((a, b) => (b.probability ?? 0.0).compareTo(a.probability ?? 0.0)); + + final topCondition = sortedConditions.first; + suggestedCondition = topCondition.commonName ?? topCondition.name ?? "Unknown"; + probability = (topCondition.probability ?? 0.0) * 100; // Convert to percentage + } + // final bool isHighConfidence = probability >= 70.0; return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -231,14 +477,14 @@ class _TriageScreenState extends State { ), SizedBox(height: 16.h), CustomRoundedProgressBar( - percentage: currentScore, + percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( - text: "$currentScore% ", + text: "${probability.toStringAsFixed(1)}% ", style: TextStyle( color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, @@ -257,6 +503,8 @@ class _TriageScreenState extends State { ), ), ), + // Show high confidence message + SizedBox(height: 12.h), Row( children: [ @@ -264,7 +512,7 @@ class _TriageScreenState extends State { child: CustomButton( text: "Previous".needTranslation, onPressed: isFirstQuestion ? () {} : _onPreviousPressed, - isDisabled: isFirstQuestion, + isDisabled: isFirstQuestion || viewModel.isTriageDiagnosisLoading, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, @@ -274,7 +522,8 @@ class _TriageScreenState extends State { SizedBox(width: 12.w), Expanded( child: CustomButton( - text: isLastQuestion ? "Finish".needTranslation : "Next".needTranslation, + text: "Next".needTranslation, + isDisabled: viewModel.isTriageDiagnosisLoading, onPressed: _onNextPressed, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index 91f3d36..b438420 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -90,8 +91,8 @@ class _UserInfoSelectionScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - title.toText16(weight: FontWeight.w500), - subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + title.toText14(weight: FontWeight.w500), + subTitle.toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), ], ), ], @@ -106,7 +107,7 @@ class _UserInfoSelectionScreenState extends State { Widget _getDivider() { return Divider( color: AppColors.dividerColor, - ).paddingSymmetrical(0, 16.h); + ).paddingSymmetrical(0, 8.h); } @override @@ -114,8 +115,25 @@ class _UserInfoSelectionScreenState extends State { AppState appState = getIt.get(); String name = ""; + int? userAgeFromDOB; if (appState.isAuthenticated) { - name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} "; + final user = appState.getAuthenticatedUser(); + name = "${user!.firstName!} ${user.lastName!} "; + + // Calculate age from authenticated user's DOB if available + if (user.dateofBirth != null && user.dateofBirth!.isNotEmpty) { + try { + DateTime dob = DateUtil.convertStringToDate(user.dateofBirth!); + final now = DateTime.now(); + int age = now.year - dob.year; + if (now.month < dob.month || (now.month == dob.month && now.day < dob.day)) { + age--; + } + userAgeFromDOB = age; + } catch (e) { + // If date parsing fails, ignore + } + } } else { name = "Guest"; } @@ -132,8 +150,9 @@ class _UserInfoSelectionScreenState extends State { // Get display values String genderText = viewModel.selectedGender ?? "Not set"; - // Show age calculated from DOB, not the DOB itself - String ageText = viewModel.selectedAge != null ? "${viewModel.selectedAge} Years" : "Not set"; + // Show age calculated from DOB (prefer viewModel's age, fallback to calculated from user's DOB) + int? displayAge = viewModel.selectedAge ?? userAgeFromDOB; + String ageText = displayAge != null ? "$displayAge Years" : "Not set"; String heightText = viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : "Not set"; String weightText = @@ -154,11 +173,11 @@ class _UserInfoSelectionScreenState extends State { padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), child: Column( children: [ - "Hello $name, Is your information up to date?".needTranslation.toText18( + "Hello $name, Is your information up to date?".needTranslation.toText16( weight: FontWeight.w600, color: AppColors.textColor, ), - SizedBox(height: 24.h), + SizedBox(height: 32.h), _buildEditInfoTile( context: context, leadingIcon: AppAssets.genderIcon, diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart index d73f387..8366545 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -35,7 +33,6 @@ class AgeSelectionPage extends StatelessWidget { initialDate: symptomsViewModel.dateOfBirth ?? DateTime(2000, 1, 1), onDateChanged: (date) { symptomsViewModel.setDateOfBirth(date); - log('DOB saved: $date, Age: ${symptomsViewModel.selectedAge}'); }, ) ], diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart index 21ad42f..59b5ce4 100644 --- a/lib/presentation/tele_consultation/zoom/call_screen.dart +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -16,7 +16,6 @@ import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk_live_transcription_message_info.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk_share_action.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart'; -import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/utils/jwt.dart'; @@ -24,7 +23,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/video_view.dart'; -import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:image_picker/image_picker.dart'; diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index ad204fa..fdfcc04 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -180,7 +180,6 @@ class _AncillaryOrderPaymentPageState extends State { ), ), ), - // Payment Summary Footer todoVM.isProcessingPayment ? SizedBox.shrink() : _buildPaymentSummary() ], @@ -220,7 +219,7 @@ class _AncillaryOrderPaymentPageState extends State { children: [ "Amount before tax".needTranslation.toText14(isBold: true), Utils.getPaymentAmountWithSymbol( - amountBeforeTax.toString().toText16(isBold: true), + amountBeforeTax.toStringAsFixed(2).toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true, @@ -234,7 +233,7 @@ class _AncillaryOrderPaymentPageState extends State { children: [ "VAT 15%".needTranslation.toText14(isBold: true, color: AppColors.greyTextColor), Utils.getPaymentAmountWithSymbol( - taxAmount.toString().toText14(isBold: true, color: AppColors.greyTextColor), + taxAmount.toStringAsFixed(2).toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, isSaudiCurrency: true, @@ -250,7 +249,7 @@ class _AncillaryOrderPaymentPageState extends State { children: [ "".needTranslation.toText14(isBold: true), Utils.getPaymentAmountWithSymbol( - widget.totalAmount.toString().toText24(isBold: true), + widget.totalAmount.toStringAsFixed(2).toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true, diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart index 940c8eb..b7515af 100644 --- a/lib/presentation/todo_section/ancillary_procedures_details_page.dart +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -13,6 +14,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_procedures_detail_response_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_order_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -20,7 +22,6 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class AncillaryOrderDetailsList extends StatefulWidget { @@ -130,7 +131,6 @@ class _AncillaryOrderDetailsListState extends State { if (viewModel.patientAncillaryOrderProceduresList.isNotEmpty) { orderData = viewModel.patientAncillaryOrderProceduresList[0]; } - return Column( children: [ Expanded( @@ -618,39 +618,102 @@ class _AncillaryOrderDetailsListState extends State { Widget _buildStickyPaymentButton(orderData) { final isButtonEnabled = selectedProcedures.isNotEmpty; - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(height: 16.h), - _buildSummarySection(orderData), - SizedBox(height: 16.h), - CustomButton( - borderWidth: 0, - backgroundColor: AppColors.infoLightColor, - text: "Proceed to Payment".needTranslation, - onPressed: () { - // Navigate to payment page with selected procedures - Navigator.of(context).push( - CustomPageRoute( - page: AncillaryOrderPaymentPage( - appointmentNoVida: widget.appointmentNoVida, - orderNo: widget.orderNo, - projectID: widget.projectID, - selectedProcedures: selectedProcedures, - totalAmount: _getTotalAmount(), - appointmentDate: orderData.appointmentDate, - ), + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: LocaleKeys.upcomingPaymentNow.tr(context: context).toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor)), + ], + ), + SizedBox(height: 18.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 150.h, + child: Utils.getPaymentMethods(), ), - ); - }, - isDisabled: !isButtonEnabled, - textColor: AppColors.whiteColor, - borderRadius: 12.r, - borderColor: Colors.transparent, - padding: EdgeInsets.symmetric(vertical: 16.h), - ), - SizedBox(height: 22.h), - ], - ).paddingSymmetrical(24.w, 0); + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Utils.getPaymentAmountWithSymbol(_getTotalAmount().toStringAsFixed(2).toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), + ], + ), + ], + ), + SizedBox(height: 16.h), + CustomButton( + borderWidth: 0, + backgroundColor: AppColors.infoLightColor, + text: "Proceed to Payment".needTranslation, + onPressed: () { + // Navigate to payment page with selected procedures + Navigator.of(context).push( + CustomPageRoute( + page: AncillaryOrderPaymentPage( + appointmentNoVida: widget.appointmentNoVida, + orderNo: widget.orderNo, + projectID: widget.projectID, + selectedProcedures: selectedProcedures, + totalAmount: _getTotalAmount(), + appointmentDate: orderData.appointmentDate, + ), + ), + ); + }, + isDisabled: !isButtonEnabled, + textColor: AppColors.whiteColor, + borderRadius: 12.r, + borderColor: Colors.transparent, + padding: EdgeInsets.symmetric(vertical: 16.h), + ), + SizedBox(height: 22.h), + ], + ).paddingOnly(left: 16.h, top: 24.h, right: 16.h, bottom: 0.h), + ); + + // Column( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // SizedBox(height: 16.h), + // _buildSummarySection(orderData), + // SizedBox(height: 16.h), + // CustomButton( + // borderWidth: 0, + // backgroundColor: AppColors.infoLightColor, + // text: "Proceed to Payment".needTranslation, + // onPressed: () { + // // Navigate to payment page with selected procedures + // Navigator.of(context).push( + // CustomPageRoute( + // page: AncillaryOrderPaymentPage( + // appointmentNoVida: widget.appointmentNoVida, + // orderNo: widget.orderNo, + // projectID: widget.projectID, + // selectedProcedures: selectedProcedures, + // totalAmount: _getTotalAmount(), + // appointmentDate: orderData.appointmentDate, + // ), + // ), + // ); + // }, + // isDisabled: !isButtonEnabled, + // textColor: AppColors.whiteColor, + // borderRadius: 12.r, + // borderColor: Colors.transparent, + // padding: EdgeInsets.symmetric(vertical: 16.h), + // ), + // SizedBox(height: 22.h), + // ], + // ).paddingSymmetrical(24.w, 0); } } diff --git a/lib/presentation/todo_section/todo_page.dart b/lib/presentation/todo_section/todo_page.dart index 11e258f..0d2d806 100644 --- a/lib/presentation/todo_section/todo_page.dart +++ b/lib/presentation/todo_section/todo_page.dart @@ -47,6 +47,7 @@ class _ToDoPageState extends State { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: 3, + padding: EdgeInsetsGeometry.zero, itemBuilder: (context, index) { return AncillaryOrderCard( order: AncillaryOrderItem(), @@ -60,14 +61,13 @@ class _ToDoPageState extends State { Widget build(BuildContext context) { appState = getIt.get(); return CollapsingListView( - title: "ToDo List".needTranslation, - isLeading: false, + title: "Ancillary Orders".needTranslation, + isLeading: true, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16.h), - "Ancillary Orders".needTranslation.toText18(isBold: true), Consumer( builder: (BuildContext context, TodoSectionViewModel todoSectionViewModel, Widget? child) { return todoSectionViewModel.isAncillaryOrdersLoading @@ -82,8 +82,9 @@ class _ToDoPageState extends State { orderNo: order.orderNo ?? 0, projectID: order.projectID ?? 0, projectName: order.projectName ?? "", - ))); - log("View details for order: ${order.orderNo}"); + ), + ), + ); }, ); }, diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index 31a778f..78d7e3e 100644 --- a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -7,9 +7,13 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/todo_section/models/resp_models/ancillary_order_list_response_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; +import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class AncillaryOrdersList extends StatelessWidget { final List orders; @@ -35,10 +39,10 @@ class AncillaryOrdersList extends StatelessWidget { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: orders.length, + padding: EdgeInsets.zero, separatorBuilder: (BuildContext context, int index) => SizedBox(height: 12.h), itemBuilder: (context, index) { final order = orders[index]; - return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), @@ -52,6 +56,7 @@ class AncillaryOrdersList extends StatelessWidget { child: AncillaryOrderCard( order: order, isLoading: false, + isOrdersList: true, onCheckIn: onCheckIn != null ? () => onCheckIn!(order) : null, onViewDetails: onViewDetails != null ? () => onViewDetails!(order) : null, )), @@ -90,12 +95,14 @@ class AncillaryOrderCard extends StatelessWidget { super.key, required this.order, this.isLoading = false, + this.isOrdersList = false, this.onCheckIn, this.onViewDetails, }); final AncillaryOrderItem order; final bool isLoading; + final bool isOrdersList; final VoidCallback? onCheckIn; final VoidCallback? onViewDetails; @@ -113,30 +120,24 @@ class AncillaryOrderCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header Row with Order Number and Date - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Row( - // children: [ - // if (!isLoading) - // "Order #".needTranslation.toText14( - // color: AppColors.textColorLight, - // weight: FontWeight.w500, - // ), - // SizedBox(width: 4.w), - // (isLoading ? "12345" : "${order.orderNo ?? '-'}").toText16(isBold: true).toShimmer2(isShow: isLoading), - // ], - // ), - // if (order.orderDate != null || isLoading) - // (isLoading ? "Jan 15, 2024" : DateFormat('MMM dd, yyyy').format(order.orderDate!)) - // .toText12(color: AppColors.textColorLight) - // .toShimmer2(isShow: isLoading), - // ], - // ), - - SizedBox(height: 12.h), - + isOrdersList + ? SizedBox.shrink() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Ancillary Orders".toText14(isBold: true), + Row( + children: [ + LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).toShimmer2(isShow: isLoading).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ToDoPage())); + }), + SizedBox(height: 10.h), // Doctor and Clinic Info Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -144,8 +145,8 @@ class AncillaryOrderCard extends StatelessWidget { if (!isLoading) ...[ Image.network( "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png", - width: 40.w, - height: 40.h, + width: 63.w, + height: 63.h, fit: BoxFit.cover, ).circle(100.r), SizedBox(width: 12.w), @@ -155,11 +156,7 @@ class AncillaryOrderCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Doctor Name - if (order.doctorName != null || isLoading) - (isLoading ? "Dr. John Smith" : order.doctorName!) - .toString() - .toText14(isBold: true, maxlines: 2) - .toShimmer2(isShow: isLoading), + if (order.doctorName != null || isLoading) (isLoading ? "Dr. John Smith" : order.doctorName!).toString().toText16(isBold: true, maxlines: 2).toShimmer2(isShow: isLoading), SizedBox(height: 4.h), ], @@ -167,9 +164,7 @@ class AncillaryOrderCard extends StatelessWidget { ), ], ), - - SizedBox(height: 12.h), - + SizedBox(height: 8.h), // Chips for Appointment Info and Status Wrap( direction: Axis.horizontal, @@ -182,18 +177,17 @@ class AncillaryOrderCard extends StatelessWidget { labelText: order.projectName ?? '-', ).toShimmer2(isShow: isLoading), // orderNo - if (order.orderNo != null || isLoading) - AppCustomChipWidget( - // icon: AppAssets.calendar, - labelText: "${"Order# :".needTranslation}${order.orderNo ?? '-'}", - ).toShimmer2(isShow: isLoading), + // if (order.orderNo != null || isLoading) + // AppCustomChipWidget( + // // icon: AppAssets.calendar, + // labelText: "${"Order# :".needTranslation}${order.orderNo ?? '-'}", + // ).toShimmer2(isShow: isLoading), // Appointment Date if (order.appointmentDate != null || isLoading) AppCustomChipWidget( - icon: AppAssets.calendar, - labelText: - isLoading ? "Date: Jan 20, 2024" : "Date: ${DateFormat('MMM dd, yyyy').format(order.appointmentDate!)}".needTranslation, + icon: AppAssets.appointment_calendar_icon, + labelText: isLoading ? "Date: Jan 20, 2024" : DateFormat('MMM dd, yyyy').format(order.appointmentDate!).needTranslation, ).toShimmer2(isShow: isLoading), // Appointment Number diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart new file mode 100644 index 0000000..bd25641 --- /dev/null +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -0,0 +1,321 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; +import 'package:provider/provider.dart'; + +class VitalSignPage extends StatefulWidget { + const VitalSignPage({super.key}); + + @override + State createState() => _VitalSignPageState(); +} + +class _VitalSignPageState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: 'Vital Signs', + child: Consumer( + builder: (context, viewModel, child) { + + // Get the latest vital sign data (first item in the list) + VitalSignResModel? latestVitalSign = viewModel.vitalSignList.isNotEmpty + ? viewModel.vitalSignList.first + : null; + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + + // Main content with body image + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left side - Vital Sign Cards + Expanded( + child: Column( + children: [ + // BMI Card + _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: 'BMI', + value: latestVitalSign?.bodyMassIndex?.toString() ?? '--', + unit: '', + status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex), + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Height Card + _buildVitalSignCard( + icon: AppAssets.heightVital, + label: 'Height', + value: latestVitalSign?.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Weight Card + _buildVitalSignCard( + icon: AppAssets.weightVital, + label: 'Weight', + value: latestVitalSign?.weightKg?.toString() ?? '--', + unit: 'kg', + status: (latestVitalSign?.weightKg != null) ? 'Normal' : null, + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Blood Pressure Card + _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: 'Blood Pressure', + value: latestVitalSign != null && + latestVitalSign.bloodPressureHigher != null && + latestVitalSign.bloodPressureLower != null + ? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}' + : '--', + unit: '', + status: VitalSignUiModel.bloodPressureStatus( + systolic: latestVitalSign?.bloodPressureHigher, + diastolic: latestVitalSign?.bloodPressureLower, + ), + onTap: () {}, + ), + SizedBox(height: 16.h), + + // Temperature Card + _buildVitalSignCard( + icon: AppAssets.temperature, + label: 'Temperature', + value: latestVitalSign?.temperatureCelcius?.toString() ?? '--', + unit: '°C', + status: null, + onTap: () {}, + ), + ], + ), + ), + + SizedBox(width: 16.h), + + // Right side - Body Image and Heart Rate + Respiratory Rate + Expanded( + child: Column( + children: [ + // Body anatomy image with Heart Rate card overlaid at bottom + SizedBox( + height: 480.h, + width: double.infinity, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Image + Positioned.fill( + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + AppAssets.bmiFullBody, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + height: 420.h, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.whiteColor.withValues(alpha: 0.0), + AppColors.whiteColor.withValues(alpha: 0.97), + AppColors.whiteColor, + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + + // Overlay Heart Rate card + Positioned( + left: 0, + right: 0, + bottom: 12.h, + child: _buildVitalSignCard( + icon: AppAssets.heart, + label: 'Heart Rate', + value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--', + unit: 'bpm', + status: 'Normal', + onTap: () {}, + ), + ), + ], + ), + ), + SizedBox(height: 12.h), + + // Respiratory rate Card + _buildVitalSignCard( + icon: AppAssets.respRate, + label: 'Respiratory rate', + value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', + unit: 'bpm', + status: 'Normal', + onTap: () {}, + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 60.h), + ], + ), + ); + }, + ), + ), + ); + } + + Widget _buildVitalSignCard({ + required String icon, + required String label, + required String value, + required String unit, + required String? status, + required VoidCallback onTap, + }) { + final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label); + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + Expanded( + child: label.toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 14.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + value.toText17( + isBold: true, + color: AppColors.textColor, + ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], + ], + ), + if (status != null) + AppCustomChipWidget( + labelText: status, + backgroundColor: scheme.chipBg, + textColor: scheme.chipFg, + ) + else + const SizedBox.shrink(), + ], + ), + ), + SizedBox(height: 8.h), + Align( + alignment: AlignmentDirectional.centerEnd, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 18.w, + height: 18.h, + iconColor: AppColors.textColorLight, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/water_monitor/water_consumption_page.dart b/lib/presentation/water_monitor/water_consumption_page.dart new file mode 100644 index 0000000..2bd429c --- /dev/null +++ b/lib/presentation/water_monitor/water_consumption_page.dart @@ -0,0 +1,840 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/hydration_tips_widget.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_intake_summary_widget.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class WaterConsumptionPage extends StatefulWidget { + const WaterConsumptionPage({super.key}); + + @override + State createState() => _WaterConsumptionPageState(); +} + +class _WaterConsumptionPageState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _refreshData(); + }); + } + + /// Refresh data by calling initialize on the view model + Future _refreshData() async { + final vm = context.read(); + await vm.initialize(); + } + + Widget _buildLoadingShimmer({bool isForHistory = true}) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(0.w), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: isForHistory ? 60.h : 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + Widget buildHistoryListTile({required String title, required String subTitle}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14(weight: FontWeight.w500, color: AppColors.labelTextColor), + subTitle.toText18(weight: FontWeight.w600), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon) + ], + ).paddingSymmetrical(0, 8.h); + } + + Widget _buildHistoryGraphOrList() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Consumer(builder: (BuildContext context, WaterMonitorViewModel viewModel, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + "History".needTranslation.toText16(isBold: true), + SizedBox(width: 8.w), + InkWell( + onTap: () => _showHistoryDurationBottomsheet(context, viewModel), + child: Container( + padding: EdgeInsets.symmetric(vertical: 4.h, horizontal: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + backgroundColor: AppColors.greyColor, + borderRadius: 8.r, + hasShadow: true, + ), + child: Row( + children: [ + viewModel.selectedDurationFilter.toText12(fontWeight: FontWeight.w500), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 16.h), + ], + ), + ), + ) + ], + ), + InkWell( + onTap: () => viewModel.setGraphView(!viewModel.isGraphView), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Container( + key: ValueKey(viewModel.isGraphView), + child: Utils.buildSvgWithAssets( + icon: viewModel.isGraphView ? AppAssets.listIcon : AppAssets.graphIcon, + height: 24.h, + width: 24.h, + ), + ), + ), + ), + ], + ), + if (!viewModel.isGraphView) _buildHistoryListView(viewModel) else ...[SizedBox(height: 16.h), _buildHistoryGraph()] + ], + ); + }), + ); + } + + Widget _buildHistoryListView(WaterMonitorViewModel viewModel) { + final selectedDuration = viewModel.selectedDurationFilter; + + // Build list items based on duration + List listItems = []; + + if (selectedDuration == 'Daily') { + if (viewModel.todayProgressList.isNotEmpty) { + final todayData = viewModel.todayProgressList.first; + listItems.add( + buildHistoryListTile( + title: "Today's Progress", + subTitle: "${todayData.quantityConsumed?.toStringAsFixed(0) ?? '0'} ml / ${todayData.quantityLimit?.toStringAsFixed(0) ?? '0'} ml", + ), + ); + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + listItems.add( + buildHistoryListTile( + title: "Percentage Completed", + subTitle: "${todayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + + // Add history data if available (show ALL entries) + if (viewModel.historyList.isNotEmpty) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + listItems.add( + Padding( + padding: EdgeInsets.symmetric(vertical: 8.h), + child: "Water Intake History".toText14( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ); + + // Show all history entries + for (var history in viewModel.historyList) { + final quantity = "${history.quantity?.toStringAsFixed(0) ?? '0'} ml"; + final time = _formatHistoryDate(history.createdDate ?? ''); + + listItems.add( + buildHistoryListTile( + title: quantity, + subTitle: time, + ), + ); + + if (history != viewModel.historyList.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for today".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } else if (selectedDuration == 'Weekly') { + if (viewModel.weekProgressList.isNotEmpty) { + // Show previous 6 days + today (total 7 days) + // API returns data in reverse order (today first), so we reverse it to show oldest to newest (top to bottom) + // This ensures today appears at the end (bottom) + final totalDays = viewModel.weekProgressList.length; + final startIndex = totalDays > 7 ? totalDays - 7 : 0; + final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList(); + + for (var dayData in weekDataToShow) { + listItems.add( + buildHistoryListTile( + title: dayData.dayName ?? 'Unknown', + subTitle: "${dayData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + if (dayData != weekDataToShow.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for this week".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } else if (selectedDuration == 'Monthly') { + if (viewModel.monthProgressList.isNotEmpty) { + // Show last 6 months + current month (total 7 months) + // Show in chronological order: oldest to newest (top to bottom) + final totalMonths = viewModel.monthProgressList.length; + final startIndex = totalMonths > 7 ? totalMonths - 7 : 0; + final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList(); + + for (var monthData in monthDataToShow) { + listItems.add( + buildHistoryListTile( + title: monthData.monthName ?? 'Unknown', + subTitle: "${monthData.percentageConsumed?.toStringAsFixed(1) ?? '0'}%", + ), + ); + if (monthData != monthDataToShow.last) { + listItems.add(Divider(height: 1, color: AppColors.dividerColor)); + } + } + } else { + listItems.add( + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: "No data available for this year".toText14(color: AppColors.greyTextColor), + ), + ), + ); + } + } + + // Return scrollable list with min and max height constraints + return ConstrainedBox( + constraints: BoxConstraints(minHeight: 80.h, maxHeight: 270.h), + child: viewModel.isLoading + ? _buildLoadingShimmer().paddingOnly(top: 16.h) + : listItems.isEmpty + ? Center( + child: "No history data available".toText14(color: AppColors.greyTextColor), + ) + : ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + itemCount: listItems.length, + separatorBuilder: (context, index) => SizedBox.shrink(), + itemBuilder: (context, index) => listItems[index], + ), + ); + } + + Widget _buildHistoryGraph() { + return Consumer( + builder: (context, viewModel, _) { + final selectedDuration = viewModel.selectedDurationFilter; + + // Build dynamic data points based on selected duration + List dataPoints = []; + + if (selectedDuration == 'Daily') { + // For daily, show last 7 history entries with at least 5 minutes difference + if (viewModel.historyList.isNotEmpty) { + // Filter entries with at least 5 minutes difference + List filteredPoints = []; + DateTime? lastTime; + + for (var historyItem in viewModel.historyList) { + final currentTime = _parseHistoryDate(historyItem.createdDate ?? ''); + + // Add if first entry OR if more than 5 minutes difference from last added entry + if (lastTime == null || currentTime.difference(lastTime).inMinutes.abs() >= 5) { + final quantity = historyItem.quantity?.toDouble() ?? 0.0; + final time = _formatHistoryDate(historyItem.createdDate ?? ''); + + filteredPoints.add( + DataPoint( + value: quantity, + actualValue: quantity.toStringAsFixed(0), + label: time, + displayTime: time, + unitOfMeasurement: 'ml', + time: currentTime, + ), + ); + lastTime = currentTime; + } + } + + // Take only last 7 filtered entries + final totalFiltered = filteredPoints.length; + final startIndex = totalFiltered > 7 ? totalFiltered - 7 : 0; + dataPoints = filteredPoints.skip(startIndex).toList(); + } else if (viewModel.todayProgressList.isNotEmpty) { + // Fallback: show today's percentage if no history + final todayData = viewModel.todayProgressList.first; + final percentage = todayData.percentageConsumed?.toDouble() ?? 0.0; + dataPoints.add( + DataPoint( + value: percentage, + actualValue: percentage.toStringAsFixed(1), + label: 'Today', + displayTime: 'Today', + unitOfMeasurement: '%', + time: DateTime.now(), + ), + ); + } + } else if (selectedDuration == 'Weekly') { + // For weekly, show previous 6 days + today (total 7 days) + // API returns data in reverse order (today first), so we reverse it to show oldest to newest (left to right) + // This ensures today appears at the end (right side) + if (viewModel.weekProgressList.isNotEmpty) { + final totalDays = viewModel.weekProgressList.length; + final startIndex = totalDays > 7 ? totalDays - 7 : 0; + final weekDataToShow = viewModel.weekProgressList.skip(startIndex).toList().reversed.toList(); + + for (var dayData in weekDataToShow) { + final percentage = dayData.percentageConsumed?.toDouble() ?? 0.0; + final dayName = dayData.dayName ?? 'Day ${dayData.dayNumber}'; + dataPoints.add( + DataPoint( + value: percentage, + actualValue: percentage.toStringAsFixed(1), + label: DateUtil.getShortWeekDayName(dayName), + displayTime: dayName, + unitOfMeasurement: '%', + time: DateTime.now(), + ), + ); + } + } + } else if (selectedDuration == 'Monthly') { + // For monthly, show last 6 months + current month (total 7 months) + // Show in chronological order: oldest to newest (left to right) + if (viewModel.monthProgressList.isNotEmpty) { + final totalMonths = viewModel.monthProgressList.length; + final startIndex = totalMonths > 7 ? totalMonths - 7 : 0; + final monthDataToShow = viewModel.monthProgressList.skip(startIndex).toList(); + + for (var monthData in monthDataToShow) { + final percentage = monthData.percentageConsumed?.toDouble() ?? 0.0; + final monthName = monthData.monthName ?? 'Month ${monthData.monthNumber}'; + dataPoints.add( + DataPoint( + value: percentage, + actualValue: percentage.toStringAsFixed(1), + label: DateUtil.getShortMonthName(monthName), + displayTime: monthName, + unitOfMeasurement: '%', + time: DateTime.now(), + ), + ); + } + } + } + + // If no data, show empty state + if (dataPoints.isEmpty) { + return Container( + padding: EdgeInsets.symmetric(vertical: 80.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.graphIcon, + iconColor: AppColors.greyTextColor.withValues(alpha: 0.5), + height: 32.w, + width: 32.w, + ), + SizedBox(height: 12.h), + "No graph data available".toText14(color: AppColors.greyTextColor), + ], + ), + ), + ); + } + + // Show loading shimmer while fetching data + if (viewModel.isLoading) { + return Container( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: _buildLoadingShimmer(), + ); + } + + // Configure graph based on selected duration + double maxY; + double minY; + double horizontalInterval; + double leftLabelInterval; + + if (selectedDuration == 'Daily') { + // For daily (quantity in ml), use max available cup size + // Get the biggest cup from available cups + final maxCupSize = viewModel.cups.isEmpty ? 500.0 : viewModel.cups.map((cup) => cup.capacityMl.toDouble()).reduce((a, b) => a > b ? a : b); + + maxY = maxCupSize; + minY = 0; + // Divide into 4 intervals (5 labels: 0, 1/4, 1/2, 3/4, max) + horizontalInterval = maxY / 4; + leftLabelInterval = maxY / 4; + } else { + // For weekly/monthly (percentage), use 0-100% + maxY = 100.0; + minY = 0; + horizontalInterval = 25; + leftLabelInterval = 25; + } + + return CustomGraph( + bottomLabelReservedSize: 30, + dataPoints: dataPoints, + makeGraphBasedOnActualValue: true, + leftLabelReservedSize: 50.h, + showGridLines: true, + maxY: maxY, + minY: minY, + showLinePoints: true, + maxX: dataPoints.length > 1 ? dataPoints.length.toDouble() - 0.75 : 1.0, + horizontalInterval: horizontalInterval, + leftLabelInterval: leftLabelInterval, + showShadow: true, + getDrawingHorizontalLine: (value) { + // Draw dashed lines at intervals + if (selectedDuration == 'Daily') { + // For daily, draw lines every 50 or 100 ml + if (value % horizontalInterval == 0 && value > 0) { + return FlLine( + color: AppColors.greyTextColor.withValues(alpha: 0.3), + strokeWidth: 1.5, + dashArray: [8, 4], + ); + } + } else { + // For weekly/monthly, draw lines at 25%, 50%, 75% + if (value == 25 || value == 50 || value == 75) { + return FlLine( + color: AppColors.successColor.withValues(alpha: 0.3), + strokeWidth: 1.5, + dashArray: [8, 4], + ); + } + } + return FlLine(color: AppColors.transparent, strokeWidth: 0); + }, + leftLabelFormatter: (value) { + if (selectedDuration == 'Daily') { + // Show exactly 5 labels: 0, 1/4, 1/2, 3/4, max + // Check if value matches one of the 5 positions + final interval = maxY / 4; + final positions = [0.0, interval, interval * 2, interval * 3, maxY]; + + for (var position in positions) { + if ((value - position).abs() < 1) { + return '${value.toInt()}ml'.toText10(weight: FontWeight.w600); + } + } + } else { + // Show percentage labels + if (value == 0) return '0%'.toText10(weight: FontWeight.w600); + if (value == 25) return '25%'.toText10(weight: FontWeight.w600); + if (value == 50) return '50%'.toText10(weight: FontWeight.w600); + if (value == 75) return '75%'.toText10(weight: FontWeight.w600); + if (value == 100) return '100%'.toText10(weight: FontWeight.w600); + } + return SizedBox.shrink(); + }, + graphColor: AppColors.successColor, + graphShadowColor: AppColors.successColor.withValues(alpha: 0.15), + bottomLabelFormatter: (value, data) { + if (data.isEmpty) return SizedBox.shrink(); + + // Only show labels for whole number positions (not fractional) + if ((value - value.round()).abs() > 0.01) { + return SizedBox.shrink(); + } + + int index = value.round(); + if (index < 0 || index >= data.length) return SizedBox.shrink(); + + // For daily, show all 7 time labels (last 7 entries) + if (selectedDuration == 'Daily' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText8( + fontWeight: FontWeight.w600, + color: AppColors.labelTextColor, + ), + ); + } + + // For weekly, show all 7 days (today + last 6 days) + if (selectedDuration == 'Weekly' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10( + weight: FontWeight.w600, + color: AppColors.labelTextColor, + ), + ); + } + + // For monthly, show all 7 months (current month + last 6 months) + if (selectedDuration == 'Monthly' && index < 7) { + return Padding( + padding: EdgeInsets.only(top: 10.h), + child: data[index].label.toText10(weight: FontWeight.w600, color: AppColors.labelTextColor), + ); + } + + return SizedBox.shrink(); + }, + scrollDirection: selectedDuration == 'Monthly' ? Axis.horizontal : Axis.vertical, + height: 250.h, + spotColor: AppColors.successColor, + ); + }, + ); + } + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + final dialogService = getIt.get(); + + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + void _showHistoryDurationBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Duration".needTranslation, + items: viewModel.durationFilters, + selectedValue: viewModel.selectedDurationFilter, + onSelected: viewModel.setFilterDuration, + ); + } + + /// Handle reminder button tap (Set or Cancel) + Future _handleReminderButtonTap(WaterMonitorViewModel viewModel) async { + if (viewModel.isWaterReminderEnabled) { + // Cancel reminders + _showCancelReminderConfirmation(viewModel); + } else { + // Set reminders + await _setReminders(viewModel); + } + } + + /// Show confirmation bottom sheet before cancelling reminders + void _showCancelReminderConfirmation(WaterMonitorViewModel viewModel) { + showCommonBottomSheetWithoutHeight( + title: 'Notice'.needTranslation, + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to cancel all water reminders?".needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + await _cancelReminders(viewModel); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + isDismissible: true, + ); + } + + /// Set water reminders + Future _setReminders(WaterMonitorViewModel viewModel) async { + // Schedule reminders + final success = await viewModel.scheduleWaterReminders(); + + if (success) { + final times = await viewModel.getScheduledReminderTimes(); + _showReminderScheduledDialog(times); + } + } + + /// Cancel water reminders + Future _cancelReminders(WaterMonitorViewModel viewModel) async { + final success = await viewModel.cancelWaterReminders(); + } + + /// Show bottom sheet with scheduled reminder times + void _showReminderScheduledDialog(List times) { + showCommonBottomSheetWithoutHeight( + title: 'Reminders Set!'.needTranslation, + context, + isCloseButtonVisible: false, + isDismissible: false, + child: Padding( + padding: EdgeInsets.only(top: 16.w, left: 16.w, right: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.getSuccessWidget(loadingText: 'Daily water reminders scheduled at:'.needTranslation), + SizedBox(height: 16.h), + Wrap( + spacing: 8.w, + runSpacing: 8.h, + children: times + .map( + (time) => AppCustomChipWidget( + icon: AppAssets.bell, + iconColor: AppColors.quickLoginColor, + richText: _formatTime(time).toText14(), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), + ), + ) + .toList(), + ), + + SizedBox(height: 24.h), + + // OK button + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: 'OK'.needTranslation, + onPressed: () => Navigator.of(context).pop(), + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + callBackFunc: () {}, + ); + } + + /// Format DateTime to readable time string + String _formatTime(DateTime time) { + final hour = time.hour; + final minute = time.minute; + final hour12 = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + return '${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period'; + } + + /// Format history date from /Date(milliseconds+0300)/ format + String _formatHistoryDate(String dateString) { + try { + // Parse the /Date(milliseconds+0300)/ format + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dateString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds); + return _formatTime(dateTime); + } + } + } catch (e) { + return dateString; + } + return dateString; + } + + /// Parse history date from /Date(milliseconds+0300)/ format to DateTime + DateTime _parseHistoryDate(String dateString) { + try { + final regex = RegExp(r'\/Date\((\d+)'); + final match = regex.firstMatch(dateString); + if (match != null) { + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds != null) { + return DateTime.fromMillisecondsSinceEpoch(milliseconds); + } + } + } catch (e) { + // Return current time as fallback + } + return DateTime.now(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Water Consumption".needTranslation, + bottomChild: Consumer( + builder: (context, viewModel, child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: viewModel.isWaterReminderEnabled ? "Cancel Reminders".needTranslation : "Set Reminder".needTranslation, + textColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor, + backgroundColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor.withValues(alpha: 0.1) : AppColors.successLightBgColor, + onPressed: () => _handleReminderButtonTap(viewModel), + icon: viewModel.isWaterReminderEnabled ? null : AppAssets.bell, + iconColor: viewModel.isWaterReminderEnabled ? AppColors.errorColor : AppColors.successColor, + borderRadius: 12.r, + borderColor: AppColors.transparent, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ); + }, + ), + child: RefreshIndicator( + onRefresh: _refreshData, + color: AppColors.blueColor, + backgroundColor: AppColors.whiteColor, + child: Column( + children: [ + SizedBox(height: 16.h), + const WaterIntakeSummaryWidget(), + SizedBox(height: 16.h), + _buildHistoryGraphOrList(), + SizedBox(height: 16.h), + const HydrationTipsWidget(), + SizedBox(height: 16.h), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/water_monitor/water_monitor_settings_page.dart b/lib/presentation/water_monitor/water_monitor_settings_page.dart new file mode 100644 index 0000000..302940c --- /dev/null +++ b/lib/presentation/water_monitor/water_monitor_settings_page.dart @@ -0,0 +1,361 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +class WaterMonitorSettingsPage extends StatefulWidget { + const WaterMonitorSettingsPage({super.key}); + + @override + State createState() => _WaterMonitorSettingsPageState(); +} + +class _WaterMonitorSettingsPageState extends State { + late DialogService dialogService; + + @override + void initState() { + super.initState(); + dialogService = getIt.get(); + } + + // No need to call initialize() here since it's already called in water_consumption_screen + // The ViewModel is shared via Provider, so data is already loaded + + // Reusable method to build selection row widget + Widget _buildSelectionRow({ + required String value, + required String groupValue, + required VoidCallback onTap, + bool useUpperCase = false, + }) { + return SizedBox( + height: 70.h, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: value, + groupValue: groupValue, + activeColor: AppColors.errorColor, + onChanged: (_) => onTap(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + (useUpperCase ? value.toUpperCase() : value.toCamelCase) + .toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1) + .expanded, + ], + ).onPress(onTap), + ); + } + + // Reusable method to show selection bottom sheet + void _showSelectionBottomSheet({ + required BuildContext context, + required String title, + required List items, + required String selectedValue, + required Function(String) onSelected, + bool useUpperCase = false, + }) { + dialogService.showFamilyBottomSheetWithoutHWithChild( + label: title.needTranslation, + message: "", + child: Container( + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildSelectionRow( + value: item, + groupValue: selectedValue, + useUpperCase: useUpperCase, + onTap: () { + onSelected(item); + Navigator.pop(context); + }, + ); + }, + separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.dividerColor), + ), + ), + onOkPressed: () {}, + ); + } + + void _showGenderSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Gender".needTranslation, + items: viewModel.genderOptions, + selectedValue: viewModel.selectedGender, + onSelected: viewModel.setGender, + ); + } + + void _showHeightUnitSelectionBottomSheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.heightUnits, + selectedValue: viewModel.selectedHeightUnit, + onSelected: viewModel.setHeightUnit, + useUpperCase: true, + ); + } + + void _showWeightUnitSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Unit".needTranslation, + items: viewModel.weightUnits, + selectedValue: viewModel.selectedWeightUnit, + onSelected: viewModel.setWeightUnit, + useUpperCase: true, + ); + } + + void _showActivityLevelSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Activity Level".needTranslation, + items: viewModel.activityLevels, + selectedValue: viewModel.selectedActivityLevel, + onSelected: viewModel.setActivityLevel, + ); + } + + void _showNumberOfRemindersSelectionBottomsheet(BuildContext context, WaterMonitorViewModel viewModel) { + _showSelectionBottomSheet( + context: context, + title: "Select Number of Reminders".needTranslation, + items: viewModel.reminderOptions, + selectedValue: viewModel.selectedNumberOfReminders, + onSelected: viewModel.setNumberOfReminders, + ); + } + + // Reusable method to build text field + Widget _buildTextField(TextEditingController controller, String hintText, {TextInputType keyboardType = TextInputType.name}) { + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: 1, + cursorHeight: 14.h, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + hintText: hintText, + hintStyle: const TextStyle(color: Colors.grey), + ), + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ); + } + + // Reusable method to build settings row + Widget _buildSettingsRow({ + required String icon, + required String label, + String? value, + Widget? inputField, + String? unit, + VoidCallback? onUnitTap, + VoidCallback? onRowTap, + bool showDivider = true, + }) { + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 40.w, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.greyColor, + borderRadius: 10.r, + hasShadow: false, + ), + child: Center(child: Utils.buildSvgWithAssets(icon: icon, height: 22.w, width: 22.w)), + ), + SizedBox(width: 12.w), + Expanded( + flex: unit != null ? 3 : 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + label.toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + if (inputField != null) + Container( + height: 20.w, + alignment: Alignment.centerLeft, + child: inputField, + ) + else if (value != null) + value.toCamelCase.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + ), + if (unit != null) ...[ + Container( + width: 1.w, + height: 30.w, + color: AppColors.dividerColor, + ).paddingOnly(right: 10.w), + Expanded( + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Unit".toText12(fontWeight: FontWeight.w500, color: AppColors.inputLabelTextColor), + unit.toText12(fontWeight: FontWeight.w500, color: AppColors.textColor), + ], + ), + SizedBox(width: 12.w), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down) + ], + ).onPress(onUnitTap ?? () {}), + ), + ] else if (onRowTap != null) ...[ + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down), + SizedBox(width: 4.w), + ], + ], + ).paddingSymmetrical(0.w, 16.w).onPress(onRowTap ?? () {}), + if (showDivider) Divider(height: 1, color: AppColors.dividerColor), + ], + ); + } + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "H20 Settings".needTranslation, + bottomChild: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.w), + child: CustomButton( + text: "Save".needTranslation, + onPressed: () async { + final success = await viewModel.saveSettings(); + if (!success && viewModel.validationError != null) { + dialogService.showErrorBottomSheet( + message: viewModel.validationError!, + ); + } else if (success) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget( + loadingText: "Settings saved successfully".needTranslation, + ), + callBackFunc: () {}, + isCloseButtonVisible: false, + isDismissible: true, + isFullScreen: false, + ); + } + }, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(vertical: 14.h), + ), + ), + ), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildSettingsRow( + icon: AppAssets.profileIcon, + label: "Your Name".needTranslation, + inputField: _buildTextField(viewModel.nameController, 'Guest'), + ), + _buildSettingsRow( + icon: AppAssets.genderIcon, + label: "Select Gender".needTranslation, + value: viewModel.selectedGender, + onRowTap: () => _showGenderSelectionBottomsheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.calendarGrey, + label: "Age (11-120) yrs".needTranslation, + inputField: _buildTextField( + viewModel.ageController, + '20', + keyboardType: TextInputType.number, + ), + ), + _buildSettingsRow( + icon: AppAssets.heightIcon, + label: "Height".needTranslation, + inputField: _buildTextField( + viewModel.heightController, + '175', + keyboardType: TextInputType.number, + ), + unit: viewModel.selectedHeightUnit, + onUnitTap: () => _showHeightUnitSelectionBottomSheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.weightScaleIcon, + label: "Weight".needTranslation, + inputField: _buildTextField( + viewModel.weightController, + '75', + keyboardType: TextInputType.number, + ), + unit: viewModel.selectedWeightUnit, + onUnitTap: () => _showWeightUnitSelectionBottomsheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.dumbellIcon, + label: "Activity Level".needTranslation, + value: viewModel.selectedActivityLevel, + onRowTap: () => _showActivityLevelSelectionBottomsheet(context, viewModel), + ), + _buildSettingsRow( + icon: AppAssets.notificationIconGrey, + label: "Number of reminders in a day".needTranslation, + value: viewModel.selectedNumberOfReminders, + onRowTap: () => _showNumberOfRemindersSelectionBottomsheet(context, viewModel), + showDivider: false, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/bottle_shape_clipper.dart b/lib/presentation/water_monitor/widgets/bottle_shape_clipper.dart new file mode 100644 index 0000000..b4b7ba0 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/bottle_shape_clipper.dart @@ -0,0 +1,25 @@ +// Add this class at the bottom of your file (outside the main class) +import 'package:flutter/material.dart'; + +class BottleShapeClipper extends CustomClipper { + @override + Path getClip(Size size) { + final path = Path(); + + // Create rounded rectangle matching the bottle body shape + // The bottle has rounded corners with radius ~30-40 based on SVG + final borderRadius = size.width * 0.25; // 25% of width for rounded corners + + path.addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + Radius.circular(borderRadius), + ), + ); + + return path; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) => false; +} diff --git a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart new file mode 100644 index 0000000..4ffa30d --- /dev/null +++ b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart @@ -0,0 +1,325 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/models/water_cup_model.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; + +/// Bottom sheet to switch between existing cups or add new custom cup +void showSwitchCupBottomSheet(BuildContext context) { + return showCommonBottomSheetWithoutHeight( + context, + titleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Switch Cups".toText20(weight: FontWeight.w600), + "Select your preferred cup size".toText16(weight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + child: SwitchCupBottomSheet(), + callBackFunc: () {}, + ); +} + +class SwitchCupBottomSheet extends StatelessWidget { + const SwitchCupBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + final viewModel = context.watch(); + final selectedId = viewModel.selectedCup?.id; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 16.w, + childAspectRatio: 0.85, + ), + itemCount: viewModel.cups.length + 1, + itemBuilder: (context, index) { + if (index == viewModel.cups.length) { + return _buildAddCupItem(context); + } + + final cup = viewModel.cups[index]; + final isSelected = selectedId == cup.id; + + return _buildCupItem( + cup: cup, + isSelected: isSelected, + onTap: () { + viewModel.selectCup(cup.id); + context.pop(); + }, + ); + }, + ), + ], + ); + } + + Widget _buildCupItem({required WaterCupModel cup, required bool isSelected, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent, + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor, + width: 1, + ), + ), + child: Center( + child: Utils.buildSvgWithAssets( + icon: cup.iconPath, + height: 30.h, + width: 42.w, + iconColor: isSelected ? AppColors.primaryRedColor : null, + ), + ), + ), + // Red badge for custom cups (delete) + if (!cup.isDefault) + Positioned( + top: -6.h, + right: -6.w, + child: Builder(builder: (ctx) { + return InkWell( + onTap: () { + // call viewmodel remove + final vm = ctx.read(); + vm.removeCup(cup.id); + }, + child: Container( + color: AppColors.whiteColor, + child: Utils.buildSvgWithAssets(icon: AppAssets.minimizeIcon, height: 20.w, width: 20.w), + ), + ); + }), + ), + ], + ), + SizedBox(height: 2.h), + '${cup.capacityMl}ml'.toText10(weight: FontWeight.w500), + ], + ), + ); + } + + Widget _buildAddCupItem(BuildContext context) { + return GestureDetector( + onTap: () { + showCustomizeCupBottomSheet(context); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 60.w, + height: 60.w, + child: Center(child: Utils.buildSvgWithAssets(icon: AppAssets.cupAdd, height: 30.h, width: 42.w)), + ), + SizedBox(height: 4.h), + 'Add'.needTranslation.toText10(weight: FontWeight.w500), + ], + ), + ); + } +} + +/// Bottom sheet to customize cup capacity with slider +void showCustomizeCupBottomSheet(BuildContext context, {WaterCupModel? cupToEdit}) { + return showCommonBottomSheetWithoutHeight( + context, + titleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Customize your drink cup".needTranslation.toText20(weight: FontWeight.w600), + ], + ), + child: CustomizeCupBottomSheet(cupToEdit: cupToEdit), + callBackFunc: () {}, + ); +} + +class CustomizeCupBottomSheet extends StatefulWidget { + final WaterCupModel? cupToEdit; + + const CustomizeCupBottomSheet({super.key, this.cupToEdit}); + + @override + State createState() => _CustomizeCupBottomSheetState(); +} + +class _CustomizeCupBottomSheetState extends State { + static const int minCapacity = 50; + static const int maxCapacity = 500; + + late double _currentCapacity; + + @override + void initState() { + super.initState(); + _currentCapacity = (widget.cupToEdit?.capacityMl ?? 150).toDouble(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Cup icon with fill level visualization + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 60.w, + height: 80.h, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // Cup image with dynamic fill + SizedBox( + width: 60.w, + height: 80.h, + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.cupEmpty, + width: 60.w, + height: 80.h, + fit: BoxFit.contain, + ), + ), + ), + ClipRect( + child: Align( + alignment: Alignment.bottomCenter, + heightFactor: (_currentCapacity / maxCapacity).clamp(0.0, 1.0), + child: Utils.buildSvgWithAssets( + icon: AppAssets.cupFilled, + width: 60.w, + height: 80.h, + fit: BoxFit.contain, + )), + ), + ], + ), + ), + + SizedBox(width: 12.w), + + // Slider and labels + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Current value + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + '${_currentCapacity.round()}'.toText32(isBold: true), + SizedBox(width: 4.w), + Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: Text( + 'ml', + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + height: 1.0, + ), + ), + ), + ], + ).paddingOnly(left: 12.w), + + SizedBox(height: 16.h), + + // Slider + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppColors.primaryRedColor, + inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2), + thumbColor: AppColors.primaryRedColor, + overlayColor: AppColors.primaryRedColor.withOpacity(0.2), + trackHeight: 4.h, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w), + ), + child: Slider( + value: _currentCapacity, + min: minCapacity.toDouble(), + max: maxCapacity.toDouble(), + divisions: (maxCapacity - minCapacity) ~/ 10, + onChanged: (value) => setState(() => _currentCapacity = value), + ), + ), + + Align( + alignment: Alignment.centerRight, + child: 'Max: $maxCapacity ml'.toText14( + color: AppColors.greyTextColor, + ), + ), + ], + ), + ), + ], + ), + + SizedBox(height: 24.h), + CustomButton( + text: 'Select'.needTranslation, + onPressed: () { + final newCup = WaterCupModel( + id: widget.cupToEdit?.id ?? Uuid().v4(), + name: '${_currentCapacity.round()}ml', + capacityMl: _currentCapacity.round(), + iconPath: AppAssets.cupEmpty, + isDefault: false, + ); + + final viewModel = context.read(); + if (widget.cupToEdit != null) { + viewModel.updateCup(newCup); + } else { + viewModel.addCup(newCup); + } + viewModel.selectCup(newCup.id); + + Navigator.pop(context); + Navigator.pop(context); + }, + backgroundColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ], + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/hydration_tips_widget.dart b/lib/presentation/water_monitor/widgets/hydration_tips_widget.dart new file mode 100644 index 0000000..df55886 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/hydration_tips_widget.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class HydrationTipsWidget extends StatelessWidget { + const HydrationTipsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.bulb_icon, + width: 24.w, + height: 24.h, + ), + SizedBox(width: 8.w), + "Tips to stay hydrated".needTranslation.toText16(isBold: true), + ], + ), + SizedBox(height: 8.h), + " • ${"Drink before you feel thirsty"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 4.h), + " • ${"Keep a refillable bottle next to you"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 4.h), + " • ${"Track your daily intake to stay motivated"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 4.h), + " • ${"Choose sparkling water instead of soda"}".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.textColorLight, + ), + SizedBox(height: 8.h), + ], + ), + ); + } +} + diff --git a/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart new file mode 100644 index 0000000..2359904 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +class WaterActionButtonsWidget extends StatelessWidget { + const WaterActionButtonsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, vm, _) { + final cupAmount = vm.selectedCupCapacityMl; + final isGoalAchieved = vm.progressPercent >= 100 || vm.nextDrinkTime.toLowerCase().contains('goal achieved'); + final isDisabled = vm.isLoading || isGoalAchieved; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Opacity( + opacity: vm.isLoading ? 0.4 : 1.0, + child: InkWell( + onTap: vm.isLoading + ? null + : () async { + if (cupAmount > 0) { + await vm.undoUserActivity(); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.minimizeIcon, + height: 20.h, + width: 20.h, + iconColor: AppColors.textColor, + ), + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor, + borderRadius: 99.r, + hasShadow: true, + ), + child: (cupAmount > 0 ? "+ $cupAmount ml" : "+ 0ml").toText12( + fontWeight: FontWeight.w600, + color: AppColors.whiteColor, + ), + ), + Opacity( + opacity: isDisabled ? 0.4 : 1.0, + child: InkWell( + onTap: isDisabled + ? null + : () async { + if (cupAmount > 0) { + await vm.insertUserActivity(quantityIntake: cupAmount); + } + }, + child: Utils.buildSvgWithAssets( + icon: AppAssets.addIconDark, + height: 20.h, + width: 20.h, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildActionButton( + context: context, + onTap: () => showSwitchCupBottomSheet(context), + overlayWidget: AppAssets.refreshIcon, + title: "Switch Cup".needTranslation, + icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w), + ), + _buildActionButton( + context: context, + onTap: () async {}, + title: "Plain Water".needTranslation, + icon: Utils.buildSvgWithAssets(icon: AppAssets.glassIcon, height: 24.w, width: 24.w), + ), + _buildActionButton( + context: context, + onTap: () => context.navigateWithName(AppRoutes.waterMonitorSettingsPage), + title: "Settings".needTranslation, + icon: Icon( + Icons.settings, + color: AppColors.blueColor, + size: 24.w, + ), + ), + ], + ), + ], + ); + }); + } + + Widget _buildActionButton({ + required BuildContext context, + String? overlayWidget, + required String title, + required Widget icon, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Column( + children: [ + Stack( + children: [ + Container( + height: 46.w, + width: 46.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor.withValues(alpha: 0.14), + borderRadius: 12.r, + hasShadow: true, + ), + child: Center(child: icon), + ), + if (overlayWidget != null) ...[ + Positioned( + top: 0, + right: 0, + child: Container( + padding: EdgeInsets.all(2.w), + height: 16.w, + width: 16.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.blueColor, + borderRadius: 100.r, + hasShadow: true, + ), + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.refreshIcon, + iconColor: AppColors.whiteColor, + ), + ), + ), + ), + ] + ], + ), + SizedBox(height: 4.h), + title.toText10(), + ], + ), + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_bottle_widget.dart b/lib/presentation/water_monitor/widgets/water_bottle_widget.dart new file mode 100644 index 0000000..b681e32 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_bottle_widget.dart @@ -0,0 +1,112 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/bottle_shape_clipper.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_consumption_progress_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +class WaterBottleWidget extends StatelessWidget { + const WaterBottleWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, vm, _) { + final progressPercent = (vm.progress * 100).clamp(0.0, 100.0); + + // SVG aspect ratio + const svgAspectRatio = 315.0 / 143.0; // ~2.2 + + // Responsive bottle sizing with device-specific constraints + double bottleWidth; + if (isTablet) { + bottleWidth = math.min(SizeUtils.width * 0.15, 180.0); + } else if (isFoldable) { + bottleWidth = math.min(100.w, 160.0); + } else { + bottleWidth = math.min(120.w, 140.0); + } + + final bottleHeight = bottleWidth * svgAspectRatio; + + // Fillable area percentages + final fillableHeightPercent = 0.7; + const fillableWidthPercent = 0.8; + + final fillableHeight = bottleHeight * fillableHeightPercent; + final fillableWidth = bottleWidth * fillableWidthPercent; + + // Device-specific positioning offsets + final double leftOffset = isTablet ? 4.w : 8.w; + final double bottomOffset = isTablet ? -65.h : -78.h; + + return SizedBox( + height: bottleHeight, + width: bottleWidth, + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + // Bottle SVG outline + Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.waterBottle, + height: bottleHeight, + width: bottleWidth, + fit: BoxFit.contain, + ), + ), + + // Wave and bubbles clipped to bottle shape + Positioned.fill( + left: leftOffset, + bottom: bottomOffset, + child: Center( + child: SizedBox( + width: fillableWidth, + height: fillableHeight, + child: ClipPath( + clipper: BottleShapeClipper(), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // Animated wave + Positioned( + child: WaterConsumptionProgressWidget( + progress: progressPercent, + size: math.min(fillableWidth, fillableHeight), + containerWidth: fillableWidth, + containerHeight: fillableHeight, + waveDuration: const Duration(milliseconds: 3000), + waveColor: AppColors.blueColor, + ), + ), + + // Bubbles (only show if progress > 10%) + if (progressPercent > 10) + Positioned( + bottom: fillableHeight * 0.12, + child: Utils.buildSvgWithAssets( + icon: AppAssets.waterBottleOuterBubbles, + height: isTablet ? math.min(45.0, fillableHeight * 0.2) : math.min(55.0, fillableHeight * 0.22), + width: fillableWidth * 0.65, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_consumption_progress_widget.dart b/lib/presentation/water_monitor/widgets/water_consumption_progress_widget.dart new file mode 100644 index 0000000..08350a1 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_consumption_progress_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_splash_progress_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class WaterConsumptionProgressWidget extends StatefulWidget { + /// progress: 0.0 - 100.0 + final double progress; + final double size; + final Color? waveColor; + final double? containerWidth; + final double? containerHeight; + final Duration? waveDuration; + + const WaterConsumptionProgressWidget({super.key, required this.progress, this.size = 100, this.waveColor, this.containerWidth, this.containerHeight, this.waveDuration}); + + @override + State createState() => _WaterConsumptionProgressWidgetState(); +} + +class _WaterConsumptionProgressWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _progressController; + late Animation _progressAnimation; + double _lastTarget = 0.0; + + @override + void initState() { + super.initState(); + _lastTarget = widget.progress; + _progressController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200)); + _progressAnimation = Tween(begin: 0, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut)); + _progressController.forward(); + } + + @override + void didUpdateWidget(covariant WaterConsumptionProgressWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if ((widget.progress - _lastTarget).abs() > 0.01) { + // animate from current value to new target + final begin = _progressAnimation.value; + _progressAnimation = + Tween(begin: begin, end: widget.progress).animate(CurvedAnimation(parent: _progressController, curve: Curves.easeInOut)); + _progressController + ..reset() + ..forward(); + _lastTarget = widget.progress; + } + } + + @override + void dispose() { + _progressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // default to app blue color if none provided + final blue = widget.waveColor ?? AppColors.blueColor; + return AnimatedBuilder( + animation: _progressAnimation, + builder: (context, child) { + return WaterWaveProgress( + progress: _progressAnimation.value, + size: widget.size, + showPercentage: false, + useCircleClip: false, + waveColor: blue, + containerWidth: widget.containerWidth, + containerHeight: widget.containerHeight, + waveDuration: widget.waveDuration, + backgroundColor: Colors.transparent, + progressColor: Colors.transparent, + textColor: Colors.transparent, + ); + }, + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_intake_summary_widget.dart b/lib/presentation/water_monitor/widgets/water_intake_summary_widget.dart new file mode 100644 index 0000000..137f6a3 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_intake_summary_widget.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_action_buttons_widget.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/widgets/water_bottle_widget.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class WaterIntakeSummaryWidget extends StatelessWidget { + const WaterIntakeSummaryWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(24.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.blueGradientColorOne, + AppColors.blueGradientColorTwo, + ], + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: isTablet ? 2 : 3, + child: Consumer(builder: (context, vm, _) { + if (vm.isLoading) { + return _buildLoadingShimmer(); + } + + final goalMl = vm.dailyGoalMl; + final consumed = vm.totalConsumedMl; + final remaining = (goalMl - consumed) > 0 ? (goalMl - consumed) : 0; + final completedPercent = "${(vm.progress * 100).clamp(0.0, 100.0).toStringAsFixed(0)}%"; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Don't show label if goal is achieved + if (!vm.nextDrinkTime.toLowerCase().contains('goal achieved')) + // Show "Tomorrow" if nextDrinkTime contains "tomorrow", otherwise "Next Drink Time" + (vm.nextDrinkTime.toLowerCase().contains('tomorrow') ? "Tomorrow" : "Next Drink Time") + .needTranslation + .toText18(weight: FontWeight.w600, color: AppColors.textColor), + + // Extract only time if "tomorrow" is present, otherwise show as is + (vm.nextDrinkTime.toLowerCase().contains('tomorrow') + ? vm.nextDrinkTime.replaceAll(RegExp(r'tomorrow', caseSensitive: false), '').trim() + : vm.nextDrinkTime) + .toText32(weight: FontWeight.w600, color: AppColors.blueColor), + + SizedBox(height: 12.h), + _buildStatusColumn(title: "Your Goal".needTranslation, subTitle: "${goalMl}ml"), + SizedBox(height: 8.h), + _buildStatusColumn(title: "Remaining".needTranslation, subTitle: "${remaining}ml"), + SizedBox(height: 8.h), + _buildStatusColumn(title: "Completed".needTranslation, subTitle: completedPercent, subTitleColor: AppColors.successColor), + SizedBox(height: 8.h), + _buildStatusColumn( + title: "Hydration Status".needTranslation, + subTitle: vm.hydrationStatus, + subTitleColor: vm.hydrationStatusColor, + ), + ], + ); + }), + ), + SizedBox(width: isTablet ? 32 : 16.w), + Expanded( + flex: isTablet ? 1 : 2, + child: const WaterBottleWidget(), + ), + ], + ), + const WaterActionButtonsWidget(), + ], + ), + ); + } + + Widget _buildStatusColumn({required String title, required String subTitle, Color? subTitleColor}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "$title: ".toText16(weight: FontWeight.w500, color: AppColors.textColor), + subTitle.toText12( + fontWeight: FontWeight.w600, + color: subTitleColor ?? AppColors.greyTextColor, + ), + ], + ); + } + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(0.w), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/water_monitor/widgets/water_splash_progress_widget.dart b/lib/presentation/water_monitor/widgets/water_splash_progress_widget.dart new file mode 100644 index 0000000..da5eb06 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_splash_progress_widget.dart @@ -0,0 +1,351 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Spherical water wave progress bar with animated ripple effect +/// Based on https://github.com/meeziest/spherical_water_wavy_progress_bar +/// +/// Usage: +/// WaterWaveProgress( +/// progress: 75.0, +/// size: 200.w, +/// showPercentage: true, +/// ) +class WaterWaveProgress extends StatefulWidget { + final double progress; // 0.0 to 100.0 + final double size; + final bool showPercentage; + final bool useCircleClip; + final Color? waveColor; + final Color? backgroundColor; + final Color? progressColor; + final Color? textColor; + final Duration? waveDuration; + + // When drawing inside a non-square clip (like bottle area), provide the actual available + // width and height so the painter can compute vertical fill correctly. + final double? containerWidth; + final double? containerHeight; + + const WaterWaveProgress({ + super.key, + required this.progress, + this.size = 200, + this.showPercentage = true, + this.useCircleClip = true, + this.waveColor, + this.backgroundColor, + this.progressColor, + this.textColor, + this.waveDuration, + this.containerWidth, + this.containerHeight, + }); + + @override + State createState() => _WaterWaveProgressState(); +} + +class _WaterWaveProgressState extends State with SingleTickerProviderStateMixin { + late AnimationController _waveController; + + @override + void initState() { + super.initState(); + _waveController = AnimationController( + vsync: this, + duration: widget.waveDuration ?? const Duration(milliseconds: 2000), + )..repeat(); + } + + @override + void dispose() { + _waveController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final radius = widget.size / 2; + + // When container dimensions are provided (non-square bottle area), use them so + // the CustomPaint canvas matches the available clip rect. Otherwise fall back + // to the square size value. + final paintWidth = widget.containerWidth ?? widget.size; + final paintHeight = widget.containerHeight ?? widget.size; + + return SizedBox( + width: paintWidth, + height: paintHeight, + child: CustomPaint( + painter: _WavePainter( + progress: widget.progress, + waveAnimation: _waveController, + // For circle-based painters we still provide circleRadius; when using + // rectangular painting the circleRadius is unused. + circleRadius: radius, + waveColor: widget.waveColor ?? AppColors.primaryRedColor, + backgroundColor: widget.backgroundColor ?? AppColors.bgScaffoldColor, + useCircleClip: widget.useCircleClip, + containerWidth: widget.containerWidth, + containerHeight: widget.containerHeight, + ), + // Only draw the circular progress/percentage when explicitly requested. + foregroundPainter: widget.showPercentage + ? _ProgressPainter( + progress: widget.progress, + circleRadius: radius, + progressColor: widget.progressColor ?? AppColors.primaryRedColor, + textColor: widget.textColor ?? AppColors.textColor, + showPercentage: widget.showPercentage, + ) + : null, + ), + ); + } +} + +/// Paints the animated water waves inside a circular clip +class _WavePainter extends CustomPainter { + final double progress; + final Animation waveAnimation; + final double circleRadius; + final Color waveColor; + final Color backgroundColor; + final bool useCircleClip; + final double? containerWidth; + final double? containerHeight; + + _WavePainter({ + required this.progress, + required this.waveAnimation, + required this.circleRadius, + required this.waveColor, + required this.backgroundColor, + this.useCircleClip = true, + this.containerWidth, + this.containerHeight, + }) : super(repaint: waveAnimation); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + canvas.translate(center.dx, center.dy); + + if (useCircleClip) { + // Clip to circle + canvas.clipPath( + Path() + ..addOval( + Rect.fromCircle( + center: Offset.zero, + radius: circleRadius, + ), + ), + ); + + // Fill background circle if specified + if (backgroundColor != Colors.transparent) { + canvas.drawCircle( + Offset.zero, + circleRadius, + Paint()..color = backgroundColor, + ); + } + + // Draw two sine waves clipped to circle for spherical style + _drawSineWave(canvas, waveColor.withAlpha((0.5 * 255).round()), shift: 0); + _drawSineWave(canvas, waveColor, shift: circleRadius / 2, mirror: true); + } else { + // No circular clipping: draw waves over the full rectangular area. Use provided container + // dimensions if available; otherwise fall back to square bounds derived from circleRadius. + final w = containerWidth ?? (circleRadius * 2); + final h = containerHeight ?? (circleRadius * 2); + + // use local transforms to center at origin and compute waves relative to the provided rect + canvas.save(); + canvas.translate(-w / 2, -h / 2); // move origin to top-left of the rect + + _drawSineWaveRect(canvas, waveColor.withAlpha((0.5 * 255).round()), width: w, height: h, shift: 0); + _drawSineWaveRect(canvas, waveColor, width: w, height: h, shift: w / 4, mirror: true); + + canvas.restore(); + } + } + + void _drawSineWaveRect(Canvas canvas, Color color, {required double width, required double height, double shift = 0.0, bool mirror = false}) { + if (mirror) { + canvas.save(); + canvas.translate(width, 0); + canvas.scale(-1, 1); + } + + final amplitude = height * 0.04; // smaller amplitude for rectangular waves + final angularVelocity = pi / (width / 2); + final delta = Curves.easeInOut.transform(progress / 100); + + final offsetX = 2 * width * waveAnimation.value + shift; + final offsetY = height * (1.0 - delta); + + final path = Path(); + for (double x = 0; x <= width; x += 1) { + final y = amplitude * sin(angularVelocity * (x + offsetX)); + if (x == 0) { + path.moveTo(x, y + offsetY); + } else { + path.lineTo(x, y + offsetY); + } + } + + path.lineTo(width, height); + path.lineTo(0, height); + path.close(); + + final wavePaint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + canvas.drawPath(path, wavePaint); + + if (mirror) canvas.restore(); + } + + void _drawSineWave(Canvas canvas, Color color, {double shift = 0.0, bool mirror = false}) { + if (mirror) { + canvas.save(); + canvas.transform(Matrix4.rotationY(pi).storage); + } + + // original circular/spherical style: compute bounds based on circleRadius + final startX = -circleRadius; + final endX = circleRadius; + final startY = circleRadius; + final endY = -circleRadius; + + final amplitude = circleRadius * 0.15; + final angularVelocity = pi / circleRadius; + final delta = Curves.easeInOut.transform(progress / 100); + + final offsetX = 2 * circleRadius * waveAnimation.value + shift; + final offsetY = startY + (endY - startY - amplitude) * delta; + + final wavePaint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final path = Path(); + + for (double x = startX; x <= endX; x++) { + // Sine wave function: y = A * sin(ωx + φ) + final y = amplitude * sin(angularVelocity * (x + offsetX)); + if (x == startX) { + path.moveTo(x, y + offsetY); + } else { + path.lineTo(x, y + offsetY); + } + } + + path.lineTo(endX, startY); + path.lineTo(startX, startY); + path.close(); + + canvas.drawPath(path, wavePaint); + + if (mirror) canvas.restore(); + } + + @override + bool shouldRepaint(covariant _WavePainter oldDelegate) => oldDelegate.progress != progress; +} + +/// Paints the circular progress arc and percentage text +class _ProgressPainter extends CustomPainter { + final double progress; + final double circleRadius; + final Color progressColor; + final Color textColor; + final bool showPercentage; + + _ProgressPainter({ + required this.progress, + required this.circleRadius, + required this.progressColor, + required this.textColor, + required this.showPercentage, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + canvas.translate(center.dx, center.dy); + + _drawCircleProgress(canvas); + if (showPercentage) { + _drawProgressText(canvas); + } + } + + void _drawCircleProgress(Canvas canvas) { + final strokeWidth = circleRadius * 0.077; + + // Background circle with shadow + final bgPaint = Paint() + ..color = progressColor.withAlpha((0.2 * 255).round()) + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..isAntiAlias = true; + + final shadowPaint = Paint() + ..color = Colors.black.withAlpha((0.1 * 255).round()) + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..maskFilter = const MaskFilter.blur(BlurStyle.outer, 10); + + canvas.drawCircle(Offset.zero, circleRadius, bgPaint); + canvas.drawCircle(Offset.zero, circleRadius, shadowPaint); + + // Progress arc + final progressPaint = Paint() + ..color = progressColor + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke + ..isAntiAlias = true; + + canvas.drawArc( + Rect.fromCircle(center: Offset.zero, radius: circleRadius), + -0.5 * pi, + 2 * pi * (progress / 100), + false, + progressPaint, + ); + } + + void _drawProgressText(Canvas canvas) { + final textSpan = TextSpan( + text: "${progress.toInt()}%", + style: TextStyle( + color: textColor, + fontSize: circleRadius * 0.35, + fontWeight: FontWeight.w700, + height: 1.0, + ), + ); + + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + )..layout(); + + textPainter.paint( + canvas, + Offset(-textPainter.width / 2, -textPainter.height / 2), + ); + } + + @override + bool shouldRepaint(covariant _ProgressPainter oldDelegate) => oldDelegate.progress != progress; +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 183c2a0..c57ffb9 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart'; +import 'package:hmg_patient_app_new/presentation/covid19test/covid19_landing_page.dart'; import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart'; +import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculators_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/add_health_tracker_entry_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_tracker_detail_page.dart'; +import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/huawei_health_example.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_instructions_page.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/risk_factors_screen.dart'; @@ -18,6 +26,9 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_screen. import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart'; import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption_page.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_page.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; class AppRoutes { @@ -29,25 +40,38 @@ class AppRoutes { static const String medicalFilePage = '/medicalFilePage'; static const String eReferralPage = '/erReferralPage'; static const String comprehensiveCheckupPage = '/comprehensiveCheckupPage'; + static const String healthCalculatorsPage = '/healthCalculatorsPage'; + static const String healthConvertersPage = '/healthConvertersPage'; static const String homeHealthCarePage = '/homeHealthCarePage'; static const String zoomCallPage = '/zoomCallPage'; static const String bloodDonationPage = '/bloodDonationPage'; + static const String smartWatches = '/smartWatches'; + static const String huaweiHealthExample = '/huaweiHealthExample'; + static const String covid19Test = '/covid19Test'; + static const String vitalSign = '/vitalSign'; //appointments static const String bookAppointmentPage = '/bookAppointmentPage'; + // Water Monitor + static const String waterConsumptionPage = '/waterConsumptionScreen'; + static const String waterMonitorSettingsPage = '/waterMonitorSettingsScreen'; + // Symptoms Checker static const String organSelectorPage = '/organSelectorPage'; - static const String symptomsSelectorScreen = '/symptomsCheckerScreen'; - static const String suggestionsScreen = '/suggestionsScreen'; - static const String riskFactorsScreen = '/riskFactorsScreen'; - static const String possibleConditionsScreen = '/possibleConditionsScreen'; - static const String triageScreen = '/triageProgressScreen'; - - //UserInfoSelection + static const String symptomsSelectorPage = '/symptomsCheckerScreen'; + static const String suggestionsPage = '/suggestionsScreen'; + static const String riskFactorsPage = '/riskFactorsScreen'; + static const String possibleConditionsPage = '/possibleConditionsScreen'; + static const String triagePage = '/triageProgressScreen'; static const String userInfoSelection = '/userInfoSelection'; static const String userInfoFlowManager = '/userInfoFlowManager'; + // Health Trackers + static const String healthTrackersPage = '/healthTrackersListScreen'; + static const String addHealthTrackerEntryPage = '/addHealthTrackerEntryPage'; + static const String healthTrackerDetailPage = '/healthTrackerDetailPage'; + static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -60,16 +84,35 @@ class AppRoutes { comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), homeHealthCarePage: (context) => HhcProceduresPage(), organSelectorPage: (context) => OrganSelectorPage(), - symptomsSelectorScreen: (context) => SymptomsSelectorScreen(), - riskFactorsScreen: (context) => RiskFactorsScreen(), - suggestionsScreen: (context) => SuggestionsScreen(), - possibleConditionsScreen: (context) => PossibleConditionsScreen(), - triageScreen: (context) => TriageScreen(), + symptomsSelectorPage: (context) => SymptomsSelectorPage(), + riskFactorsPage: (context) => RiskFactorsScreen(), + suggestionsPage: (context) => SuggestionsScreen(), + possibleConditionsPage: (context) => PossibleConditionsPage(), + triagePage: (context) => TriagePage(), bloodDonationPage: (context) => BloodDonationPage(), bookAppointmentPage: (context) => BookAppointmentPage(), userInfoSelection: (context) => UserInfoSelectionScreen(), userInfoFlowManager: (context) => UserInfoFlowManager(), - - // + smartWatches: (context) => SmartwatchInstructionsPage(), + huaweiHealthExample: (context) => HuaweiHealthExample(), + covid19Test: (context) => Covid19LandingPage(), + waterConsumptionPage: (context) => WaterConsumptionPage(), + waterMonitorSettingsPage: (context) => WaterMonitorSettingsPage(), + healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), + healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), + healthTrackersPage: (context) => HealthTrackersPage(), + vitalSign: (context) => VitalSignPage(), + addHealthTrackerEntryPage: (context) { + final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?; + return AddHealthTrackerEntryPage( + trackerType: args ?? HealthTrackerTypeEnum.bloodSugar, + ); + }, + healthTrackerDetailPage: (context) { + final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?; + return HealthTrackerDetailPage( + trackerType: args ?? HealthTrackerTypeEnum.bloodSugar, + ); + }, }; } diff --git a/lib/services/analytics/analytics_service.dart b/lib/services/analytics/analytics_service.dart index 3411157..c8f2e48 100644 --- a/lib/services/analytics/analytics_service.dart +++ b/lib/services/analytics/analytics_service.dart @@ -1,8 +1,5 @@ import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:geolocator/geolocator.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/advance_payments.dart'; @@ -15,7 +12,6 @@ import 'package:hmg_patient_app_new/services/analytics/flows/live_care.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/login_registration.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/offers_promotions.dart'; import 'package:hmg_patient_app_new/services/analytics/flows/todo_list.dart'; -import 'package:http/http.dart' as AnalyticEvents; typedef GALogger = Function(String name, {Map parameters}); diff --git a/lib/services/analytics/flows/advance_payments.dart b/lib/services/analytics/flows/advance_payments.dart index 9c8baa1..133e241 100644 --- a/lib/services/analytics/flows/advance_payments.dart +++ b/lib/services/analytics/flows/advance_payments.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; class AdvancePayments{ diff --git a/lib/services/analytics/flows/appointments.dart b/lib/services/analytics/flows/appointments.dart index 57c45e6..d9991a6 100644 --- a/lib/services/analytics/flows/appointments.dart +++ b/lib/services/analytics/flows/appointments.dart @@ -1,8 +1,5 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; -import 'package:intl/intl.dart'; class Appointment { final GALogger logger; diff --git a/lib/services/analytics/flows/live_care.dart b/lib/services/analytics/flows/live_care.dart index 5e7f330..c5fac58 100644 --- a/lib/services/analytics/flows/live_care.dart +++ b/lib/services/analytics/flows/live_care.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; class LiveCare{ diff --git a/lib/services/analytics/flows/login_registration.dart b/lib/services/analytics/flows/login_registration.dart index 9488324..611cc47 100644 --- a/lib/services/analytics/flows/login_registration.dart +++ b/lib/services/analytics/flows/login_registration.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; class LoginRegistration{ diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 497a009..3c009f3 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -62,18 +62,16 @@ class DialogServiceImp implements DialogService { message: message, showCancel: onCancelPressed != null ? true : false, onOkPressed: () { - print('ok button is pressed'); if (onOkPressed != null) { - print('onOkPressed is not null'); onOkPressed(); - }else { + } else { Navigator.pop(context); } }, onCancelPressed: () { if (onCancelPressed != null) { onCancelPressed(); - }else { + } else { Navigator.pop(context); } }, @@ -108,7 +106,8 @@ class DialogServiceImp implements DialogService { } @override - Future showCommonBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { + Future showCommonBottomSheetWithoutH( + {String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight( @@ -162,7 +161,8 @@ class DialogServiceImp implements DialogService { } @override - Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { + Future showPhoneNumberPickerSheet( + {String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight(context, @@ -184,7 +184,8 @@ class DialogServiceImp implements DialogService { } } -Widget exceptionBottomSheetWidget({required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { +Widget exceptionBottomSheetWidget( + {required BuildContext context, required String message, required Function() onOkPressed, Function()? onCancelPressed}) { return Column( children: [ (message).toText16(isBold: false, color: AppColors.textColor), @@ -239,7 +240,8 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin ); } -Widget showPhoneNumberPickerWidget({required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { +Widget showPhoneNumberPickerWidget( + {required BuildContext context, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) { return StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { return Column( children: [ diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 0000000..fd154c6 --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,373 @@ +import 'dart:typed_data'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz show TZDateTime, local, setLocalLocation, getLocation; + +/// Abstract class defining the notification service interface +abstract class NotificationService { + /// Initialize the notification service + Future initialize({Function(String payload)? onNotificationClick}); + + /// Request notification permissions (mainly for iOS) + Future requestPermissions(); + + /// Show an immediate notification + Future showNotification({ + required String title, + required String body, + String? payload, + }); + + /// Schedule a notification at a specific date and time + Future scheduleNotification({ + required int id, + required String title, + required String body, + required DateTime scheduledDate, + String? payload, + }); + + /// Schedule daily notifications at specific times + Future scheduleDailyNotifications({ + required List times, + required String title, + required String body, + String? payload, + }); + + /// Schedule water reminder notifications + Future scheduleWaterReminders({ + required List reminderTimes, + required String title, + required String body, + }); + + /// Cancel a specific notification by id + Future cancelNotification(int id); + + /// Cancel all scheduled notifications + Future cancelAllNotifications(); + + /// Get list of pending notifications + Future> getPendingNotifications(); +} + +/// Implementation of NotificationService following the project architecture +class NotificationServiceImp implements NotificationService { + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + final LoggerService loggerService; + + NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService}); + + // Channel IDs for different notification types + static const String _waterReminderChannelId = 'water_reminder_channel'; + static const String _waterReminderChannelName = 'Water Reminders'; + static const String _waterReminderChannelDescription = 'Daily water intake reminders'; + + static const String _generalChannelId = 'hmg_general_channel'; + static const String _generalChannelName = 'HMG Notifications'; + static const String _generalChannelDescription = 'General notifications from HMG'; + + Function(String payload)? _onNotificationClick; + + @override + Future initialize({Function(String payload)? onNotificationClick}) async { + try { + // Initialize timezone database + tz.initializeTimeZones(); + + // Set local timezone (you can also use a specific timezone if needed) + // For example: tz.setLocalLocation(tz.getLocation('Asia/Riyadh')); + final locationName = DateTime.now().timeZoneName; + try { + tz.setLocalLocation(tz.getLocation(locationName)); + } catch (e) { + // Fallback to UTC if specific timezone not found + loggerService.logInfo('Could not set timezone $locationName, using UTC'); + tz.setLocalLocation(tz.getLocation('UTC')); + } + + _onNotificationClick = onNotificationClick; + + const androidSettings = AndroidInitializationSettings('app_icon'); + const iosSettings = DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const initializationSettings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: _handleNotificationResponse, + ); + + loggerService.logInfo('NotificationService initialized successfully'); + } catch (ex) { + loggerService.logError('Failed to initialize NotificationService: $ex'); + } + } + + /// Handle notification tap + void _handleNotificationResponse(NotificationResponse response) { + try { + if (response.payload != null && _onNotificationClick != null) { + _onNotificationClick!(response.payload!); + } + loggerService.logInfo('Notification tapped: ${response.payload}'); + } catch (ex) { + loggerService.logError('Error handling notification response: $ex'); + } + } + + @override + Future requestPermissions() async { + try { + // Request permissions for iOS + final result = + await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + + // For Android 13+, permissions are requested at runtime + final androidResult = await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.requestNotificationsPermission(); + + loggerService.logInfo('Notification permissions: iOS=${result ?? true}, Android=${androidResult ?? true}'); + return result ?? androidResult ?? true; + } catch (ex) { + loggerService.logError('Error requesting notification permissions: $ex'); + return false; + } + } + + @override + Future showNotification({ + required String title, + required String body, + String? payload, + }) async { + try { + final androidDetails = AndroidNotificationDetails( + _generalChannelId, + _generalChannelName, + channelDescription: _generalChannelDescription, + importance: Importance.high, + priority: Priority.high, + vibrationPattern: _getVibrationPattern(), + ); + + const iosDetails = DarwinNotificationDetails(); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await flutterLocalNotificationsPlugin.show( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + title, + body, + notificationDetails, + payload: payload, + ); + + loggerService.logInfo('Notification shown: $title'); + } catch (ex) { + loggerService.logError('Error showing notification: $ex'); + } + } + + @override + Future scheduleNotification({ + required int id, + required String title, + required String body, + required DateTime scheduledDate, + String? payload, + }) async { + try { + final androidDetails = AndroidNotificationDetails( + _generalChannelId, + _generalChannelName, + channelDescription: _generalChannelDescription, + importance: Importance.high, + priority: Priority.high, + vibrationPattern: _getVibrationPattern(), + ); + + const iosDetails = DarwinNotificationDetails(); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await flutterLocalNotificationsPlugin.zonedSchedule( + id, + title, + body, + tz.TZDateTime.from(scheduledDate, tz.local), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + payload: payload, + ); + + loggerService.logInfo('Notification scheduled for: $scheduledDate'); + } catch (ex) { + loggerService.logError('Error scheduling notification: $ex'); + } + } + + @override + Future scheduleDailyNotifications({ + required List times, + required String title, + required String body, + String? payload, + }) async { + try { + for (int i = 0; i < times.length; i++) { + final time = times[i]; + await scheduleNotification( + id: i + 1000, + // Offset ID to avoid conflicts + title: title, + body: body, + scheduledDate: time, + payload: payload, + ); + } + + loggerService.logInfo('Scheduled ${times.length} daily notifications'); + } catch (ex) { + loggerService.logError('Error scheduling daily notifications: $ex'); + } + } + + @override + Future scheduleWaterReminders({ + required List reminderTimes, + required String title, + required String body, + }) async { + try { + // Cancel existing water reminders first + await _cancelWaterReminders(); + + final androidDetails = AndroidNotificationDetails( + _waterReminderChannelId, + _waterReminderChannelName, + channelDescription: _waterReminderChannelDescription, + importance: Importance.high, + priority: Priority.high, + vibrationPattern: _getVibrationPattern(), + icon: 'app_icon', + styleInformation: const BigTextStyleInformation(''), + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + for (int i = 0; i < reminderTimes.length; i++) { + final reminderTime = reminderTimes[i]; + final notificationId = 5000 + i; // Use 5000+ range for water reminders + + // Schedule for today if time hasn't passed, otherwise schedule for tomorrow + DateTime scheduledDate = reminderTime; + if (scheduledDate.isBefore(DateTime.now())) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + + await flutterLocalNotificationsPlugin.zonedSchedule( + notificationId, + title, + body, + tz.TZDateTime.from(scheduledDate, tz.local), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the same time + payload: 'water_reminder_$i', + ); + } + + loggerService.logInfo('Scheduled ${reminderTimes.length} water reminders'); + } catch (ex) { + loggerService.logError('Error scheduling water reminders: $ex'); + } + } + + /// Cancel all water reminders (IDs 5000-5999) + Future _cancelWaterReminders() async { + try { + final pendingNotifications = await getPendingNotifications(); + for (final notification in pendingNotifications) { + if (notification.id >= 5000 && notification.id < 6000) { + await cancelNotification(notification.id); + } + } + loggerService.logInfo('Cancelled all water reminders'); + } catch (ex) { + loggerService.logError('Error cancelling water reminders: $ex'); + } + } + + @override + Future cancelNotification(int id) async { + try { + await flutterLocalNotificationsPlugin.cancel(id); + loggerService.logInfo('Cancelled notification with ID: $id'); + } catch (ex) { + loggerService.logError('Error cancelling notification: $ex'); + } + } + + @override + Future cancelAllNotifications() async { + try { + await flutterLocalNotificationsPlugin.cancelAll(); + loggerService.logInfo('Cancelled all notifications'); + } catch (ex) { + loggerService.logError('Error cancelling all notifications: $ex'); + } + } + + @override + Future> getPendingNotifications() async { + try { + final pending = await flutterLocalNotificationsPlugin.pendingNotificationRequests(); + loggerService.logInfo('Found ${pending.length} pending notifications'); + return pending; + } catch (ex) { + loggerService.logError('Error getting pending notifications: $ex'); + return []; + } + } + + /// Get vibration pattern for notifications + Int64List _getVibrationPattern() { + final vibrationPattern = Int64List(4); + vibrationPattern[0] = 0; + vibrationPattern[1] = 500; + vibrationPattern[2] = 500; + vibrationPattern[3] = 500; + return vibrationPattern; + } +} diff --git a/lib/splashPage.dart b/lib/splashPage.dart index b5b752b..043eefd 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -1,41 +1,34 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_callkit_incoming/entities/call_event.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; import 'package:get_it/get_it.dart'; -import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; -import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; -import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; - -// import 'package:hmg_patient_app_new/presentation/authantication/login.dart'; -import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; +import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; -import 'package:provider/provider.dart'; import 'core/cache_consts.dart'; -import 'core/utils/local_notifications.dart'; import 'core/utils/push_notification_handler.dart'; -import 'widgets/routes/custom_page_route.dart'; class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + @override _SplashScreenState createState() => _SplashScreenState(); } @@ -54,9 +47,13 @@ class _SplashScreenState extends State { ); await authVm.getServicePrivilege(); Timer(Duration(seconds: 2, milliseconds: 500), () async { - bool isAppOpenedFromCall = await GetIt.instance().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; + bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; - LocalNotification.init(onNotificationClick: (payload) {}); + // Initialize NotificationService using dependency injection + final notificationService = getIt.get(); + await notificationService.initialize(onNotificationClick: (payload) { + // Handle notification click here + }); if (isAppOpenedFromCall) { navigateToTeleConsult(); @@ -85,7 +82,8 @@ class _SplashScreenState extends State { // GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); - Navigator.of(GetIt.instance().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.of(GetIt.instance().navigatorKey.currentContext!) + .pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); Navigator.pushReplacementNamed( // context, GetIt.instance().navigatorKey.currentContext!, @@ -218,12 +216,12 @@ class _SplashScreenState extends State { // AppSharedPreferences().setString(APP_LANGUAGE, projectProvider.isArabic ? "ar" : "en"); // var themeNotifier = Provider.of(context, listen: false); // themeNotifier.setTheme(defaultTheme(fontName: projectProvider.isArabic ? 'Cairo' : 'Poppins')); - PushNotificationHandler().init(context); // Asyncronously + // PushNotificationHandler().init(context); // Asyncronously } @override void initState() { - authVm = context.read(); + authVm = getIt(); super.initState(); initializeStuff(); } @@ -232,6 +230,8 @@ class _SplashScreenState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.whiteColor, - body: Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill).center); + body: Lottie.asset(AppAnimations.loadingAnimation, + repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill) + .center); } } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 630cf02..aee425c 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -2,96 +2,112 @@ import 'package:flutter/material.dart'; class AppColors { static const transparent = Colors.transparent; - static const mainPurple = Color(0xFF7954F7); + static const mainPurple = Color(0xFF7954F7); // #7954F7 - static const scaffoldBgColor = Color(0xFFF8F8F8); - static const bottomSheetBgColor = Color(0xFFF8F8FA); - static const lightGreyEFColor = Color(0xffeaeaff); - static const greyF7Color = Color(0xffF7F7F7); - static const lightGrayColor = Color(0xff808080); - static const greyTextColorLight = Color(0xFFA2A2A2); + static const scaffoldBgColor = Color(0xFFF8F8F8); // #F8F8F8 + static const bottomSheetBgColor = Color(0xFFF8F8FA); // #F8F8FA + static const lightGreyEFColor = Color(0xffeaeaff); // #EAEAFF + static const greyF7Color = Color(0xffF7F7F7); // #F7F7F7 + static const greyInfoTextColor = Color(0xff777777); // #777777 + static const lightGrayColor = Color(0xff808080); // #808080 + static const greyTextColorLight = Color(0xFFA2A2A2); // #A2A2A2 // New UI Colors - static const whiteColor = Color(0xFFffffff); - static const Color bgScaffoldColor = Color(0xffF8F8F8); - static const Color primaryRedColor = Color(0xFFED1C2B); - static const Color primaryRedBorderColor = Color(0xFFED1C2B); - static const Color secondaryLightRedColor = Color(0xFFFEE9EA); - static const Color secondaryLightRedBorderColor = Color(0xFFFEE9EA); - static const Color bgRedLightColor = Color(0xFFFEE9EA); - static const Color bgGreenColor = Color(0xFF18C273); - static const Color textColor = Color(0xFF2E3039); - static const Color borderGrayColor = Color(0x332E3039); - static const Color textColorLight = Color(0xFF5E5E5E); - static const Color borderOnlyColor = Color(0xFF2E3039); - static const Color chipBorderColorOpacity20 = Color(0x332E3039); - static const Color dividerColor = Color(0x40D2D2D2); - static const Color warningColorYellow = Color(0xFFF4A308); - static const Color blackBgColor = Color(0xFF2E3039); - static const blackColor = textColor; - static const Color inputLabelTextColor = Color(0xff898A8D); - static const Color greyTextColor = Color(0xFF8F9AA3); - static const Color lightGrayBGColor = Color(0x142E3039); - - static const lightGreenColor = Color(0xFF0ccedde); - static const textGreenColor = Color(0xFF18C273); - static const Color ratingColorYellow = Color(0xFFFFAF15); - static const Color spacerLineColor = Color(0x2E30391A); + static const whiteColor = Color(0xFFffffff); // #FFFFFF + static const Color bgScaffoldColor = Color(0xffF8F8F8); // #F8F8F8 + static const Color primaryRedColor = Color(0xFFED1C2B); // #ED1C2B + static const Color primaryRedBorderColor = Color(0xFFED1C2B); // #ED1C2B + static const Color secondaryLightRedColor = Color(0xFFFEE9EA); // #FEE9EA + static const Color secondaryLightRedBorderColor = Color(0xFFFEE9EA); // #FEE9EA + static const Color bgRedLightColor = Color(0xFFFEE9EA); // #FEE9EA + static const Color bgGreenColor = Color(0xFF18C273); // #18C273 + static const Color textColor = Color(0xFF2E3039); // #2E3039 + static const Color borderGrayColor = Color(0x332E3039); // #2E3039 (20% opacity) + static const Color textColorLight = Color(0xFF5E5E5E); // #5E5E5E + static const Color borderOnlyColor = Color(0xFF2E3039); // #2E3039 + static const Color chipBorderColorOpacity20 = Color(0x332E3039); // #2E3039 (20% opacity) + static const Color dividerColor = Color(0x40D2D2D2); // #D2D2D2 (25% opacity) + static const Color warningColorYellow = Color(0xFFF4A308); // #F4A308 + static const Color blackBgColor = Color(0xFF2E3039); // #2E3039 + static const blackColor = textColor; // #2E3039 + static const Color inputLabelTextColor = Color(0xff898A8D); // #898A8D + static const Color greyTextColor = Color(0xFF8F9AA3); // #8F9AA3 + static const Color lightGrayBGColor = Color(0x142E3039); // #2E3039 (8% opacity) + static const Color checkBoxBorderColor = Color(0xffD2D2D2); // #D2D2D2 + + static const Color pharmacyBGColor = Color(0xFF359846); // #359846 + + static const lightGreenColor = Color(0xFF0ccedde); // #0CCEDDE + static const textGreenColor = Color(0xFF18C273); // #18C273 + static const Color ratingColorYellow = Color(0xFFFFAF15); // #FFAF15 + static const Color spacerLineColor = Color(0x2E30391A); // #2E3039 (10% opacity) //Chips - static const Color successColor = Color(0xff18C273); - static const Color errorColor = Color(0xFFED1C2B); - static const Color alertColor = Color(0xFFD48D05); - static const Color infoColor = Color(0xFF0B85F7); - static const Color warningColor = Color(0xFFFFCC00); - static const Color greyColor = Color(0xFFEFEFF0); - static const Color chipPrimaryRedBorderColor = Color(0xFFED1C2B); - static const Color chipSecondaryLightRedColor = Color(0xFFFEE9EA); - - static const Color successLightColor = Color(0xFF18C273); - static const Color errorLightColor = Color(0xFFED1C2B); - static const Color alertLightColor = Color(0xFFD48D05); - static const Color infoLightColor = Color(0xFF0B85F7); - static const Color warningLightColor = Color(0xFFFFCC00); - static const Color greyLightColor = Color(0xFFEFEFF0); - static const Color thumbColor = Color(0xFF18C273); - static const Color switchBackgroundColor = Color(0x2618C273); - - static const Color bottomNAVBorder = Color(0xFFEEEEEE); - - static const Color quickLoginColor = Color(0xFF666666); - - static const Color tooltipTextColor = Color(0xFF414D55); - static const Color graphGridColor = Color(0x4D18C273); - static const Color criticalLowAndHigh = Color(0xFFED1C2B); - static const Color highAndLow = Color(0xFFFFAF15); - static const Color labelTextColor = Color(0xFF838383); - static const Color calenderTextColor = Color(0xFFD0D0D0); - static const Color lightGreenButtonColor = Color(0x2618C273); - - static const Color lightRedButtonColor = Color(0x1AED1C2B); + static const Color successColor = Color(0xff18C273); // #18C273 + static const Color successLightBgColor = Color(0xffDDF6EA); // #DDF6EA + static const Color errorColor = Color(0xFFED1C2B); // #ED1C2B + static const Color alertColor = Color(0xFFD48D05); // #D48D05 + static const Color infoColor = Color(0xFF0B85F7); // #0B85F7 + static const Color warningColor = Color(0xFFFFCC00); // #FFCC00 + static const Color greyColor = Color(0xFFEFEFF0); // #EFEFF0 + static const Color chipPrimaryRedBorderColor = Color(0xFFED1C2B); // #ED1C2B + static const Color chipSecondaryLightRedColor = Color(0xFFFEE9EA); // #FEE9EA + // static const Color chipSecondaryLightRedColor = Color(0xFFFF9E15); // #FEE9EA + + static const Color successLightColor = Color(0xFF18C273); // #18C273 + static const Color errorLightColor = Color(0xFFED1C2B); // #ED1C2B + static const Color alertLightColor = Color(0xFFD48D05); // #D48D05 + static const Color infoLightColor = Color(0xFF0B85F7); // #0B85F7 + static const Color warningLightColor = Color(0xFFFFCC00); // #FFCC00 + static const Color greyLightColor = Color(0xFFEFEFF0); // #EFEFF0 + static const Color thumbColor = Color(0xFF18C273); // #18C273 + static const Color switchBackgroundColor = Color(0x2618C273); // #18C273 (15% opacity) + + static const Color bottomNAVBorder = Color(0xFFEEEEEE); // #EEEEEE + + static const Color quickLoginColor = Color(0xFF666666); // #666666 + + static const Color tooltipTextColor = Color(0xFF414D55); // #414D55 + static const Color graphGridColor = Color(0x4D18C273); // #18C273 (30% opacity) + static const Color criticalLowAndHigh = Color(0xFFED1C2B); // #ED1C2B + static const Color highAndLow = Color(0xFFFFAF15); // #FFAF15 + static const Color labelTextColor = Color(0xFF838383); // #838383 + static const Color calenderTextColor = Color(0xFFD0D0D0); // #D0D0D0 + static const Color lightGreenButtonColor = Color(0x2618C273); // #18C273 (15% opacity) + + static const Color lightRedButtonColor = Color(0x1AED1C2B); // #ED1C2B (10% opacity) // Status Colors - static const Color statusPendingColor = Color(0xffCC9B14); - static const Color statusProcessingColor = Color(0xff2E303A); - static const Color statusCompletedColor = Color(0xff359846); - static const Color statusRejectedColor = Color(0xffD02127); + static const Color statusPendingColor = Color(0xffCC9B14); // #CC9B14 + static const Color statusProcessingColor = Color(0xff2E303A); // #2E303A + static const Color statusCompletedColor = Color(0xff359846); // #359846 + static const Color statusRejectedColor = Color(0xffD02127); // #D02127 // Info Banner Colors - static const Color infoBannerBgColor = Color(0xFFFFF4E6); - static const Color infoBannerBorderColor = Color(0xFFFFE5B4); - static const Color infoBannerIconColor = Color(0xFFCC9B14); - static const Color infoBannerTextColor = Color(0xFF856404); + static const Color infoBannerBgColor = Color(0xFFFFF4E6); // #FFF4E6 + static const Color infoBannerBorderColor = Color(0xFFFFE5B4); // #FFE5B4 + static const Color infoBannerIconColor = Color(0xFFCC9B14); // #CC9B14 + static const Color infoBannerTextColor = Color(0xFF856404); // #856404 // SymptomsChecker - static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); - static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103); - static const Color chipColorMonitor = Color(0xFF18C273); - static const Color chipColorEmergency = Color(0xFFED1C2B); + static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); // #FFAF15 + static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103); // #AB7103 + static const Color chipColorMonitor = Color(0xFF18C273); // #18C273 + static const Color chipColorEmergency = Color(0xFFED1C2B); // #ED1C2B // Services Page Colors - static const Color eReferralCardColor = Color(0xFFFF8012); - static const Color bloodDonationCardColor = Color(0xFFFF5662); - static const Color bookAppointment = Color(0xFF415364); + static const Color eReferralCardColor = Color(0xFFFF8012); // #FF8012 + static const Color bloodDonationCardColor = Color(0xFFFF5662); // #FF5662 + static const Color bookAppointment = Color(0xFF415364); // #415364 + + // Water Monitor + static const Color blueColor = Color(0xFF4EB5FF); // #4EB5FF + static const Color blueGradientColorOne = Color(0xFFF1F7FD); // #F1F7FD + static const Color blueGradientColorTwo = Color(0xFFD9EFFF); // #D9EFFF + + // Shimmer + static const Color shimmerBaseColor = Color(0xFFE0E0E0); // #E0E0E0 + static const Color shimmerHighlightColor = Color(0xFFF5F5F5); // #F5F5F5 + static const Color covid29Color = Color(0xff2563EB); // #2563EB } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index 7329de9..5409fcf 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -1,4 +1,3 @@ -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -22,6 +21,7 @@ class CollapsingListView extends StatelessWidget { VoidCallback? history; VoidCallback? instructions; VoidCallback? requests; + VoidCallback? sendEmail; Widget? bottomChild; Widget? trailing; bool isClose; @@ -41,12 +41,15 @@ class CollapsingListView extends StatelessWidget { this.history, this.instructions, this.requests, + this.sendEmail, this.isLeading = true, this.trailing, this.leadingCallback, this.physics, }); + final ScrollController _controller = ScrollController(); + @override Widget build(BuildContext context) { AppState appState = getIt.get(); @@ -56,20 +59,37 @@ class CollapsingListView extends StatelessWidget { children: [ CustomScrollView( physics: physics, + controller: _controller, slivers: [ SliverAppBar( - automaticallyImplyLeading: false, + automaticallyImplyLeading: isLeading, pinned: true, - expandedHeight: MediaQuery.of(context).size.height * 0.11.h, - stretch: true, + // toolbarHeight: isLeading ? 24.h : kToolbarHeight, + leadingWidth: isLeading ? null : double.infinity, systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), surfaceTintColor: Colors.transparent, backgroundColor: AppColors.bgScaffoldColor, + bottom: isLeading + ? ScrollAnimatedTitle( + title: title, + showBack: true, + controller: _controller, + search: search, + report: report, + logout: logout, + history: history, + instructions: instructions, + requests: requests, + sendEmail: sendEmail, + bottomChild: bottomChild, + trailing: trailing, + ) + : null, leading: isLeading ? Transform.flip( flipX: appState.isArabic(), child: IconButton( - icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), + icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 24.h, height: 24.h), padding: EdgeInsets.only(left: 12), onPressed: () { if (leadingCallback != null) { @@ -81,68 +101,20 @@ class CollapsingListView extends StatelessWidget { highlightColor: Colors.transparent, ), ) - : SizedBox.shrink(), - flexibleSpace: LayoutBuilder( - builder: (context, constraints) { - final double maxHeight = 100.h; - final double minHeight = kToolbarHeight; - double t = (constraints.maxHeight - minHeight) / (maxHeight - minHeight); - t = t - 1; - if (t < 0.7) t = 0.7; - t = t.clamp(0.0, 1.0); - - final double fontSize = lerpDouble(14, 18, t)!; - final double bottomPadding = lerpDouble(0, 0, t)!; - final double leftPadding = lerpDouble(150, 24, t)!; - - return Stack( - children: [ - Align( - alignment: Alignment.lerp( - Alignment.center, - Alignment.bottomLeft, - t, - )!, - child: Padding( - padding: EdgeInsets.only( - left: appState.isArabic() ? 0 : leftPadding, right: appState.isArabic() ? leftPadding : 0, bottom: bottomPadding), - child: Row( - spacing: 4.h, - children: [ - Text( - title, - maxLines: 1, - style: TextStyle( - fontSize: (27 - (5 * (2 - t))).f, - fontWeight: FontWeight.lerp( - FontWeight.w300, - FontWeight.w600, - t, - )!, - color: AppColors.blackColor, - letterSpacing: -0.5), - ).expanded, - if (logout != null) - actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(logout!), - if (report != null) - actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(report!), - if (history != null) - actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon) - .onPress(history!), - if (instructions != null) - actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(instructions!), - if (requests != null) - actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon) - .onPress(requests!), - if (search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(search!).paddingOnly(right: 24), - if (trailing != null) trailing!, - ], - )), - ), - ], - ); - }, - ), + : ScrollAnimatedTitle( + title: title, + showBack: false, + controller: _controller, + search: search, + report: report, + logout: logout, + history: history, + instructions: instructions, + requests: requests, + sendEmail: sendEmail, + bottomChild: bottomChild, + trailing: trailing, + ), ), SliverList( delegate: SliverChildBuilderDelegate( @@ -158,13 +130,165 @@ class CollapsingListView extends StatelessWidget { ); } +// Widget actionButton(BuildContext context, double t, {required String title, required String icon}) { +// return AnimatedSize( +// duration: Duration(milliseconds: 150), +// child: Container( +// height: 40.h, +// padding: EdgeInsets.all(8.w), +// margin: EdgeInsets.only(right: 24.w), +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration( +// color: AppColors.secondaryLightRedColor, +// borderRadius: 10.r, +// ), +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.center, +// mainAxisAlignment: MainAxisAlignment.center, +// mainAxisSize: MainAxisSize.min, +// spacing: 8.h, +// children: [ +// Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.primaryRedColor), +// if (t == 1) +// Text( +// title, +// style: context.dynamicTextStyle( +// color: AppColors.primaryRedColor, +// letterSpacing: -0.4, +// fontSize: (12 - (2 * (1 - t))).f, +// fontWeight: FontWeight.lerp( +// FontWeight.w300, +// FontWeight.w500, +// t, +// )!, +// ), +// ), +// ], +// ), +// ), +// ); +// } +} +// +// class ActionModel { +// bool requireT; +// +// } + +class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget { + final String title; + final bool showBack; + final ScrollController controller; + + VoidCallback? search; + VoidCallback? report; + VoidCallback? logout; + VoidCallback? history; + VoidCallback? instructions; + VoidCallback? requests; + VoidCallback? sendEmail; + Widget? bottomChild; + Widget? trailing; + + ScrollAnimatedTitle({ + super.key, + required this.title, + required this.controller, + required this.showBack, + this.search, + this.report, + this.logout, + this.history, + this.instructions, + this.requests, + this.sendEmail, + this.bottomChild, + this.trailing, + }); + + @override + Size get preferredSize => const Size.fromHeight(50); + + @override + State createState() => _ScrollAnimatedTitleState(); +} + +class _ScrollAnimatedTitleState extends State { + static const double _maxFont = 26; + static const double _minFont = 18; + + static const double _maxHeight = 80; + static const double _minHeight = 56; + + double _fontSize = _maxFont; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onScroll); + } + + @override + void dispose() { + widget.controller.removeListener(_onScroll); + super.dispose(); + } + + double t = 0; + + void _onScroll() { + final double offset = widget.controller.offset; + + // control animation range + const double range = 120; + + final double t = (1 - (offset / range)).clamp(0.0, 1.0); + this.t = t; + setState(() { + _fontSize = _minFont + (_maxFont - _minFont) * t; + }); + } + + @override + Widget build(BuildContext context) { + final isRtl = Directionality.of(context) == TextDirection.rtl; + return Container( + height: (widget.preferredSize.height - _fontSize / 2).h, + alignment: isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), + padding: EdgeInsets.fromLTRB(24, 0, 24, 0), + child: Row( + spacing: 4.h, + children: [ + Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: _fontSize, + fontWeight: FontWeight.bold, + letterSpacing: -1.0, + ), + ).expanded, + ...[ + if (widget.logout != null) actionButton(context, t, title: "Logout".needTranslation, icon: AppAssets.logout).onPress(widget.logout!), + if (widget.report != null) actionButton(context, t, title: "Feedback".needTranslation, icon: AppAssets.report_icon).onPress(widget.report!), + if (widget.history != null) actionButton(context, t, title: "History".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.history!), + if (widget.instructions != null) actionButton(context, t, title: "Instructions".needTranslation, icon: AppAssets.requests).onPress(widget.instructions!), + if (widget.requests != null) actionButton(context, t, title: "Requests".needTranslation, icon: AppAssets.insurance_history_icon).onPress(widget.requests!), + if (widget.sendEmail != null) actionButton(context, t, title: "Send Email".needTranslation, icon: AppAssets.email).onPress(widget.sendEmail!), + if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!), + if (widget.trailing != null) widget.trailing!, + ] + ], + ), + ); + } + Widget actionButton(BuildContext context, double t, {required String title, required String icon}) { return AnimatedSize( duration: Duration(milliseconds: 150), child: Container( - height: 40.h, + height: 36.h + (4.h * t), padding: EdgeInsets.all(8.w), - margin: EdgeInsets.only(right: 24.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.secondaryLightRedColor, borderRadius: 10.r, @@ -176,7 +300,7 @@ class CollapsingListView extends StatelessWidget { spacing: 8.h, children: [ Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.primaryRedColor), - if (t == 1) + if (t >= .5) Text( title, style: context.dynamicTextStyle( @@ -184,7 +308,7 @@ class CollapsingListView extends StatelessWidget { letterSpacing: -0.4, fontSize: (12 - (2 * (1 - t))).f, fontWeight: FontWeight.lerp( - FontWeight.w300, + FontWeight.w400, FontWeight.w500, t, )!, diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index a3e117f..2da1d44 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -21,7 +21,7 @@ class BottomNavigation extends StatelessWidget { final items = [ BottomNavItem(icon: AppAssets.homeBottom, label: LocaleKeys.home.tr(context: context)), appState.isAuthenticated - ? BottomNavItem(icon: AppAssets.myFilesBottom, label: LocaleKeys.myFiles.tr(context: context)) + ? BottomNavItem(icon: AppAssets.myFilesBottom, label: LocaleKeys.medicalFile.tr(context: context)) : BottomNavItem(icon: AppAssets.feedback, label: LocaleKeys.feedback.tr()), BottomNavItem( icon: AppAssets.bookAppoBottom, @@ -30,7 +30,8 @@ class BottomNavigation extends StatelessWidget { isSpecial: true, ), appState.isAuthenticated - ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) + // ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) + ? BottomNavItem(icon: AppAssets.symptomCheckerBottomIcon, label: "Symptoms") : BottomNavItem(icon: AppAssets.news, label: LocaleKeys.news.tr()), BottomNavItem(icon: AppAssets.servicesBottom, label: LocaleKeys.services2.tr(context: context)), ]; @@ -39,7 +40,7 @@ class BottomNavigation extends StatelessWidget { decoration: _containerDecoration, padding: _containerPadding, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( items.length, (index) => _buildNavItem(items[index], index), @@ -61,12 +62,12 @@ class BottomNavigation extends StatelessWidget { child: Utils.buildSvgWithAssets( icon: item.icon, height: item.iconSize.h, - width: item.iconSize.h, + width: item.iconSize.w, // iconColor: isSelected ? Colors.black87 : Colors.black87, ), ), const SizedBox(height: 10), - item.label.toText12(fontWeight: FontWeight.w500), + item.label.toText11(weight: FontWeight.w500), SizedBox(height: item.isSpecial ? 5 : 0) ], ), @@ -84,7 +85,7 @@ class BottomNavItem { const BottomNavItem({ required this.icon, required this.label, - this.iconSize = 21, + this.iconSize = 24, this.isSpecial = false, this.color, }); diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index 676f0bc..08d281f 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -75,7 +75,7 @@ class CustomButton extends StatelessWidget { children: [ if (icon != null) Padding( - padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero, + padding: text.isNotEmpty ? EdgeInsets.only(right: 8.w, left: 8.w) : EdgeInsets.zero, child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled, width: iconS, height: iconS), ), Visibility( diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 8be16b7..8597b3e 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -72,13 +72,13 @@ class AppCustomChipWidget extends StatelessWidget { ? Image.asset(icon, width: iconS, height: iconS) : Utils.buildSvgWithAssets( icon: icon, - width: iconS, - height: iconS, - iconColor: iconHasColor ? iconColor : null, - fit: BoxFit.contain, - ) + width: iconS, + height: iconS, + iconColor: iconHasColor ? iconColor : null, + fit: BoxFit.contain, + ) : SizedBox.shrink(), - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), + label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, labelPadding: labelPadding ?? EdgeInsetsDirectional.only(end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w), @@ -104,7 +104,7 @@ class AppCustomChipWidget extends StatelessWidget { ) : Chip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? labelText!.toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor, isCenter: true), + label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor, isCenter: true), padding: EdgeInsets.zero, backgroundColor: backgroundColor, shape: shape ?? diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index 9312519..6ff5cc5 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -108,11 +108,11 @@ class ButtonSheetContent extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // SizedBox( - // height: 20.h, - // ), + SizedBox( + height: 20.h, + ), // Center( // child: Container( // margin: const EdgeInsets.only(top: 18, bottom: 12), @@ -127,10 +127,9 @@ class ButtonSheetContent extends StatelessWidget { // Close button isCloseButtonVisible && isFullScreen - ? Column(children: [ - SizedBox( - height: 40.h, - ), + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ Padding( padding: EdgeInsets.symmetric( horizontal: 16, @@ -145,7 +144,7 @@ class ButtonSheetContent extends StatelessWidget { isFullScreen ? Column( children: [ - SizedBox(height: 20.h), + // SizedBox(height: 20.h), Padding(padding: EdgeInsets.symmetric(horizontal: 16.h), child: title.toText24(isBold: true)), SizedBox(height: 16.h), ], diff --git a/lib/widgets/custom_tab_bar.dart b/lib/widgets/custom_tab_bar.dart index 35aafa4..c30c070 100644 --- a/lib/widgets/custom_tab_bar.dart +++ b/lib/widgets/custom_tab_bar.dart @@ -14,7 +14,7 @@ class CustomTabBarModel { } class CustomTabBar extends StatefulWidget { - final int initialIndex = 0; + final int initialIndex; final List tabs; final Color activeTextColor; final Color activeBackgroundColor; @@ -25,6 +25,7 @@ class CustomTabBar extends StatefulWidget { const CustomTabBar({ super.key, required this.tabs, + this.initialIndex = 0, this.activeTextColor = const Color(0xff2E3039), this.inActiveTextColor = const Color(0xff898A8D), this.activeBackgroundColor = const Color(0x142E3039), @@ -41,6 +42,7 @@ class CustomTabBarState extends State { @override void initState() { + selectedIndex = widget.initialIndex; super.initState(); } diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index 8620ab5..debc069 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -14,7 +14,6 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; -import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; typedef OnRangeSelected = void Function(DateTime? start, DateTime? end); diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart index d295987..3f67b6a 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart @@ -1,4 +1,3 @@ -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/lab/models/Range.dart'; diff --git a/lib/widgets/datepicker_widget.dart b/lib/widgets/datepicker_widget.dart index 47c4de6..6ca9fe1 100644 --- a/lib/widgets/datepicker_widget.dart +++ b/lib/widgets/datepicker_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; class DatePickerWidget extends StatelessWidget { diff --git a/lib/widgets/family_files/family_file_add_widget.dart b/lib/widgets/family_files/family_file_add_widget.dart index 8e57f91..4840ba7 100644 --- a/lib/widgets/family_files/family_file_add_widget.dart +++ b/lib/widgets/family_files/family_file_add_widget.dart @@ -1,5 +1,4 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index b955b32..ad47cd2 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; + /// A customizable line graph widget using `fl_chart`. /// /// Displays a line chart with configurable axis labels, colors, and data points. @@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// /// **Parameters:** /// - [dataPoints]: List of `DataPoint` objects to plot. +/// - [secondaryDataPoints]: Optional list for a second line (e.g., diastolic in blood pressure). /// - [leftLabelFormatter]: Function to build left axis labels. /// - [bottomLabelFormatter]: Function to build bottom axis labels. /// - [width]: Optional width of the chart. @@ -17,6 +19,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// - [maxY], [maxX], [minX]: Axis bounds. /// - [spotColor]: Color of the touched spot marker. /// - [graphColor]: Color of the line. +/// - [secondaryGraphColor]: Color of the secondary line. /// - [graphShadowColor]: Color of the area below the line. /// - [graphGridColor]: Color of the grid lines. /// - [bottomLabelColor]: Color of bottom axis labels. @@ -42,6 +45,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// ) class CustomGraph extends StatelessWidget { final List dataPoints; + final List? secondaryDataPoints; // For dual-line graphs (e.g., blood pressure) final double? width; final double height; final double? maxY; @@ -49,6 +53,7 @@ class CustomGraph extends StatelessWidget { final double? minX; final Color spotColor; final Color graphColor; + final Color? secondaryGraphColor; // Color for secondary line final Color graphShadowColor; final Color graphGridColor; final Color bottomLabelColor; @@ -56,67 +61,73 @@ class CustomGraph extends StatelessWidget { final FontWeight? bottomLabelFontWeight; final double? leftLabelInterval; final double? leftLabelReservedSize; + final double? bottomLabelReservedSize; final bool? showGridLines; final GetDrawingGridLine? getDrawingHorizontalLine; final double? horizontalInterval; final double? minY; final bool showShadow; + final bool showLinePoints; + final double? cutOffY; final RangeAnnotations? rangeAnnotations; ///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart final Widget Function(double) leftLabelFormatter; - final Widget Function(double , List) bottomLabelFormatter; - + final Widget Function(double, List) bottomLabelFormatter; final Axis scrollDirection; final bool showBottomTitleDates; final bool isFullScreeGraph; final bool makeGraphBasedOnActualValue; - const CustomGraph({ - super.key, - required this.dataPoints, - required this.leftLabelFormatter, - this.width, - required this.scrollDirection, - required this.height, - this.maxY, - this.maxX, - this.showBottomTitleDates = true, - this.isFullScreeGraph = false, - this.spotColor = AppColors.bgGreenColor, - this.graphColor = AppColors.bgGreenColor, - this.graphShadowColor = AppColors.graphGridColor, - this.graphGridColor = AppColors.graphGridColor, - this.bottomLabelColor = AppColors.textColor, - this.bottomLabelFontWeight = FontWeight.w500, - this.bottomLabelSize, - this.leftLabelInterval, - this.leftLabelReservedSize, - this.makeGraphBasedOnActualValue = false, - required this.bottomLabelFormatter, - this.minX, - this.showGridLines = false, - this.getDrawingHorizontalLine, - this.horizontalInterval, - this.minY, - this.showShadow = false, - this.rangeAnnotations - }); + const CustomGraph( + {super.key, + required this.dataPoints, + this.secondaryDataPoints, + required this.leftLabelFormatter, + this.width, + required this.scrollDirection, + required this.height, + this.maxY, + this.maxX, + this.showBottomTitleDates = true, + this.isFullScreeGraph = false, + this.spotColor = AppColors.bgGreenColor, + this.graphColor = AppColors.bgGreenColor, + this.secondaryGraphColor, + this.graphShadowColor = AppColors.graphGridColor, + this.graphGridColor = AppColors.graphGridColor, + this.bottomLabelColor = AppColors.textColor, + this.bottomLabelFontWeight = FontWeight.w500, + this.bottomLabelSize, + this.leftLabelInterval, + this.leftLabelReservedSize, + this.bottomLabelReservedSize, + this.makeGraphBasedOnActualValue = false, + required this.bottomLabelFormatter, + this.minX, + this.showGridLines = false, + this.getDrawingHorizontalLine, + this.horizontalInterval, + this.minY, + this.showShadow = false, + this.showLinePoints = false, + this.cutOffY = 0, + this.rangeAnnotations}); @override Widget build(BuildContext context) { return Material( - color: Colors.white, - child: SizedBox( - width: width, - height: height, - child: LineChart( - LineChartData( - minY: minY??0, + color: Colors.white, + child: SizedBox( + width: width, + height: height, + child: LineChart( + LineChartData( + minY: minY ?? 0, maxY: maxY, maxX: maxX, - minX: minX , + minX: minX, lineTouchData: LineTouchData( getTouchLineEnd: (_, __) => 0, getTouchedSpotIndicator: (barData, indicators) { @@ -149,11 +160,8 @@ class CustomGraph extends StatelessWidget { final dataPoint = dataPoints[spot.x.toInt()]; return LineTooltipItem( - '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement??""} - ${dataPoint.displayTime}', - TextStyle( - color: Colors.black, - fontSize: 12.f, - fontWeight: FontWeight.w500), + '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', + TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), ); } return null; // hides the rest @@ -165,7 +173,7 @@ class CustomGraph extends StatelessWidget { leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: leftLabelReservedSize??80, + reservedSize: leftLabelReservedSize ?? 80, interval: leftLabelInterval ?? .1, // Let fl_chart handle it getTitlesWidget: (value, _) { return leftLabelFormatter(value); @@ -176,9 +184,9 @@ class CustomGraph extends StatelessWidget { axisNameSize: 20, sideTitles: SideTitles( showTitles: showBottomTitleDates, - reservedSize: 20, + reservedSize: bottomLabelReservedSize ?? 20, getTitlesWidget: (value, _) { - return bottomLabelFormatter(value, dataPoints, ); + return bottomLabelFormatter(value, dataPoints); }, interval: 1, // ensures 1:1 mapping with spots ), @@ -195,36 +203,35 @@ class CustomGraph extends StatelessWidget { top: BorderSide.none, ), ), - lineBarsData: _buildColoredLineSegments(dataPoints), + lineBarsData: _buildColoredLineSegments(dataPoints, showLinePoints), gridData: FlGridData( - show: showGridLines??true, + show: showGridLines ?? true, drawVerticalLine: false, - horizontalInterval:horizontalInterval, + horizontalInterval: horizontalInterval, // checkToShowHorizontalLine: (value) => // value >= 0 && value <= 100, - getDrawingHorizontalLine: getDrawingHorizontalLine??(value) { - return FlLine( - color: graphGridColor, - strokeWidth: 1, - dashArray: [5, 5], - ); - }, + getDrawingHorizontalLine: getDrawingHorizontalLine ?? + (value) { + return FlLine( + color: graphGridColor, + strokeWidth: 1, + dashArray: [5, 5], + ); + }, ), - rangeAnnotations: rangeAnnotations - ), - ), + rangeAnnotations: rangeAnnotations), ), + ), ); } - - List _buildColoredLineSegments(List dataPoints) { + List _buildColoredLineSegments(List dataPoints, bool showLinePoints) { final List allSpots = dataPoints.asMap().entries.map((entry) { - double value = (makeGraphBasedOnActualValue)?double.tryParse(entry.value.actualValue)??0.0:entry.value.value; + double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; return FlSpot(entry.key.toDouble(), value); }).toList(); - var data = [ + var data = [ LineChartBarData( spots: allSpots, isCurved: true, @@ -236,11 +243,11 @@ class CustomGraph extends StatelessWidget { begin: Alignment.centerLeft, end: Alignment.centerRight, ), - dotData: FlDotData( - show: false, - ), + dotData: FlDotData(show: showLinePoints), belowBarData: BarAreaData( show: showShadow, + applyCutOffY: cutOffY != null, + cutOffY: cutOffY ?? 0, gradient: LinearGradient( colors: [ graphShadowColor, @@ -253,6 +260,31 @@ class CustomGraph extends StatelessWidget { ) ]; + // Add secondary line if provided (for dual-line graphs like blood pressure) + if (secondaryDataPoints != null && secondaryDataPoints!.isNotEmpty) { + final List secondarySpots = secondaryDataPoints!.asMap().entries.map((entry) { + double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; + return FlSpot(entry.key.toDouble(), value); + }).toList(); + + data.add( + LineChartBarData( + spots: secondarySpots, + isCurved: true, + isStrokeCapRound: true, + isStrokeJoinRound: true, + barWidth: 2, + gradient: LinearGradient( + colors: [secondaryGraphColor ?? AppColors.blueColor, secondaryGraphColor ?? AppColors.blueColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + dotData: FlDotData(show: showLinePoints), + belowBarData: BarAreaData(show: false), + ), + ); + } + return data; } -} \ No newline at end of file +} diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 7992ece..9f101d4 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -3,17 +3,14 @@ import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; - -import '../core/dependencies.dart'; - -// TODO: Import AppColors if bgRedColor is defined there -// import 'package:hmg_patient_app_new/core/ui_utils/app_colors.dart'; +import 'package:hmg_patient_app_new/widgets/time_picker_widget.dart'; class TextInputWidget extends StatelessWidget { final String labelText; @@ -48,6 +45,8 @@ class TextInputWidget extends StatelessWidget { final int minLines; final int maxLines; final Color? hintColor; + final bool? isHideSwitcher; + final bool? isArrowTrailing; // final List countryList; // final Function(Country)? onCountryChange; @@ -85,6 +84,8 @@ class TextInputWidget extends StatelessWidget { this.isMultiline = false, this.minLines = 3, this.maxLines = 6, + this.isHideSwitcher, + this.isArrowTrailing, // this.countryList = const [], // this.onCountryChange, }); @@ -165,7 +166,8 @@ class TextInputWidget extends StatelessWidget { ], ), ), - if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context), + if (selectionType == SelectionTypeEnum.calendar) _buildTrailingIcon(context, isArrowTrailing: isArrowTrailing ?? false), + if (selectionType == SelectionTypeEnum.time) _buildTimePickerIcon(context, isArrowTrailing: isArrowTrailing ?? false), if (selectionType == SelectionTypeEnum.search) _buildTrailingIconForSearch(context), ], ), @@ -198,7 +200,7 @@ class TextInputWidget extends StatelessWidget { child: Utils.buildSvgWithAssets(icon: leadingIcon!)); } - Widget _buildTrailingIcon(BuildContext context) { + Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) { final AppState appState = getIt.get(); return Container( height: 40.h, @@ -214,6 +216,7 @@ class TextInputWidget extends StatelessWidget { switcherIcon: Utils.buildSvgWithAssets(icon: AppAssets.language, width: 24.h, height: 24.h), language: appState.getLanguageCode()!, initialDate: DateTime.now(), + showCalendarToggle: isHideSwitcher == true ? false : true, fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins", okWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), @@ -230,7 +233,47 @@ class TextInputWidget extends StatelessWidget { onChange!(picked.toString()); } }, - child: Utils.buildSvgWithAssets(icon: AppAssets.calendar), + child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.calendar), + ), + ); + } + + Widget _buildTimePickerIcon(BuildContext context, {bool isArrowTrailing = false}) { + return Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.zero, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.whiteColor, + ), + child: GestureDetector( + onTap: () async { + // Parse existing time if available + TimeOfDay? initialTime; + if (controller?.text.isNotEmpty ?? false) { + initialTime = TimePickerWidget.parseTime(controller!.text); + } + + final picked = await TimePickerWidget.show( + context, + initialTime: initialTime, + use24HourFormat: false, // You can make this configurable if needed + onTimeSelected: (time) { + if (onChange != null) { + final formattedTime = TimePickerWidget.formatTime(time); + onChange!(formattedTime); + } + }, + ); + + // Update controller if time was picked + if (picked != null && controller != null) { + controller!.text = TimePickerWidget.formatTime(picked); + } + }, + child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.alarm_clock_icon), ), ); } diff --git a/lib/widgets/loading_dialog.dart b/lib/widgets/loading_dialog.dart index 3c2440a..dc860b5 100644 --- a/lib/widgets/loading_dialog.dart +++ b/lib/widgets/loading_dialog.dart @@ -3,10 +3,7 @@ import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:lottie/lottie.dart'; class LoadingDialog extends StatefulWidget { diff --git a/lib/widgets/map/HMSMap.dart b/lib/widgets/map/HMSMap.dart index 7b9c553..96b2fac 100644 --- a/lib/widgets/map/HMSMap.dart +++ b/lib/widgets/map/HMSMap.dart @@ -1,9 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:huawei_map/huawei_map.dart' ; diff --git a/lib/widgets/map/gms_map.dart b/lib/widgets/map/gms_map.dart index 6d04692..f4c3ad5 100644 --- a/lib/widgets/map/gms_map.dart +++ b/lib/widgets/map/gms_map.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; diff --git a/lib/widgets/map/location_map_widget.dart b/lib/widgets/map/location_map_widget.dart index 4cf5eae..c0eb431 100644 --- a/lib/widgets/map/location_map_widget.dart +++ b/lib/widgets/map/location_map_widget.dart @@ -21,7 +21,7 @@ class LocationMapWidget extends StatelessWidget { final String address; /// The title to show above the map (e.g., "Service Location", "Hospital Location") - final String title; + final String? title; /// The zoom level for the map (default: 14) final int zoomLevel; @@ -36,9 +36,6 @@ class LocationMapWidget extends StatelessWidget { /// Whether to show the address container (default: true) final bool showAddress; - /// Whether to show the title (default: true) - final bool showTitle; - /// Custom map type (default: roadmap) final String mapType; final EdgeInsets? padding; @@ -48,12 +45,11 @@ class LocationMapWidget extends StatelessWidget { required this.latitude, required this.longitude, required this.address, - required this.title, + this.title, this.zoomLevel = 14, this.mapSize = '350x165', this.onDirectionsTap, this.showAddress = true, - this.showTitle = true, this.mapType = 'roadmap', this.padding, }); @@ -70,18 +66,15 @@ class LocationMapWidget extends StatelessWidget { "https://maps.googleapis.com/maps/api/staticmap?center=$latitude,$longitude&zoom=$zoomLevel&size=$mapSize&maptype=$mapType&markers=color:red%7C$latitude,$longitude&key=${ApiKeyConstants.googleMapsApiKey}"; return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.r, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), padding: padding ?? EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title - if (showTitle) ...[ + if (title != null) ...[ Text( - title, + title ?? "", style: TextStyle( fontSize: 16.f, fontWeight: FontWeight.w700, @@ -94,35 +87,22 @@ class LocationMapWidget extends StatelessWidget { // Address display if (showAddress) ...[ - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.h), - decoration: BoxDecoration( - color: AppColors.bgScaffoldColor, - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: AppColors.greyColor.withAlpha(51), - width: 1, - ), - ), - child: Row( - children: [ - Icon(Icons.location_on, color: AppColors.primaryRedColor, size: 20.h), - SizedBox(width: 8.w), - Expanded( - child: Text( - address, - style: TextStyle( - fontSize: 14.f, - fontWeight: FontWeight.w500, - color: AppColors.blackColor, - letterSpacing: -0.4, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Row( + children: [ + Expanded( + child: Text( + address, + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w500, + color: AppColors.blackColor, + letterSpacing: -0.4, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - ], - ), + ), + ], ), SizedBox(height: 16.h), ], diff --git a/lib/widgets/shimmer/common_shimmer_widget.dart b/lib/widgets/shimmer/common_shimmer_widget.dart index d6a2906..3d935cb 100644 --- a/lib/widgets/shimmer/common_shimmer_widget.dart +++ b/lib/widgets/shimmer/common_shimmer_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -11,9 +12,7 @@ class CommonShimmerWidget extends StatelessWidget { return SizedBox( child: Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), + borderRadius: BorderRadius.all(Radius.circular(10.r)), border: Border.all(color: AppColors.lightGreyEFColor, width: 1), boxShadow: [ BoxShadow( @@ -27,7 +26,11 @@ class CommonShimmerWidget extends StatelessWidget { padding: const EdgeInsets.all(12.0), child: Column( children: [ - Container(height: 100).toShimmer(), + Container( + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(24.r)), + )).toShimmer(), 16.height, Container(height: 24).toShimmer(), 16.height, diff --git a/lib/widgets/shimmer/vital_sign_shimmer_widget.dart b/lib/widgets/shimmer/vital_sign_shimmer_widget.dart new file mode 100644 index 0000000..2ab4741 --- /dev/null +++ b/lib/widgets/shimmer/vital_sign_shimmer_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class VitalSignShimmerWidget extends StatelessWidget { + const VitalSignShimmerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // BMI Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Height Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Weight Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Blood Pressure Card Shimmer + Expanded(child: _buildShimmerCard()), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Padding( + padding: EdgeInsets.all(12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Icon shimmer + Container( + width: 32.w, + height: 32.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + ), + ).toShimmer(), + SizedBox(height: 8.h), + // Label shimmer + Container( + width: 50.w, + height: 10.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Value shimmer + Container( + width: 40.w, + height: 16.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Chip shimmer + Container( + width: 45.w, + height: 18.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Arrow shimmer + Align( + alignment: AlignmentDirectional.centerEnd, + child: Container( + width: 10.w, + height: 10.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + ), + ).toShimmer(), + ), + ], + ), + ), + ); + } +} + diff --git a/lib/widgets/time_picker_widget.dart b/lib/widgets/time_picker_widget.dart new file mode 100644 index 0000000..71d9ab2 --- /dev/null +++ b/lib/widgets/time_picker_widget.dart @@ -0,0 +1,348 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +/// A reusable time picker widget that can be used anywhere in the app +/// Shows a bottom sheet with iOS-style time picker +class TimePickerWidget { + /// Shows a time picker bottom sheet + /// + /// [context] - BuildContext for showing the bottom sheet + /// [initialTime] - Initial time to display (defaults to current time) + /// [use24HourFormat] - Whether to use 24-hour format (defaults to false) + /// [onTimeSelected] - Callback when time is selected + /// + /// Returns the selected TimeOfDay or null if cancelled + static Future show( + BuildContext context, { + TimeOfDay? initialTime, + bool use24HourFormat = false, + bool displaySelectedTime = false, + Function(TimeOfDay)? onTimeSelected, + }) async { + final selectedTime = initialTime ?? TimeOfDay.now(); + + final result = await showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (BuildContext context) { + return _TimePickerBottomSheet( + initialTime: selectedTime, + use24HourFormat: use24HourFormat, + displaySelectedTime: displaySelectedTime, + onTimeChanged: (time) { + // Time is being changed in real-time + }, + ); + }, + ); + + if (result != null && onTimeSelected != null) { + onTimeSelected(result); + } + + return result; + } + + /// Formats TimeOfDay to string (HH:mm format) + static String formatTime(TimeOfDay time, {bool use24HourFormat = false}) { + if (use24HourFormat) { + return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } else { + final hour = time.hourOfPeriod == 0 ? 12 : time.hourOfPeriod; + final period = time.period == DayPeriod.am ? 'AM' : 'PM'; + return '${hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')} $period'; + } + } + + /// Parses time string to TimeOfDay + static TimeOfDay? parseTime(String timeString) { + try { + final parts = timeString.split(':'); + if (parts.length == 2) { + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1].split(' ')[0]); + return TimeOfDay(hour: hour, minute: minute); + } + } catch (e) { + return null; + } + return null; + } +} + +class _TimePickerBottomSheet extends StatefulWidget { + final TimeOfDay initialTime; + final bool use24HourFormat; + final bool displaySelectedTime; + final Function(TimeOfDay) onTimeChanged; + + const _TimePickerBottomSheet({ + required this.initialTime, + required this.use24HourFormat, + required this.displaySelectedTime, + required this.onTimeChanged, + }); + + @override + State<_TimePickerBottomSheet> createState() => _TimePickerBottomSheetState(); +} + +class _TimePickerBottomSheetState extends State<_TimePickerBottomSheet> { + late int selectedHour; + late int selectedMinute; + late DayPeriod selectedPeriod; + + @override + void initState() { + super.initState(); + selectedHour = widget.use24HourFormat ? widget.initialTime.hour : widget.initialTime.hourOfPeriod; + if (selectedHour == 0 && !widget.use24HourFormat) selectedHour = 12; + selectedMinute = widget.initialTime.minute; + selectedPeriod = widget.initialTime.period; + } + + TimeOfDay _getCurrentTime() { + if (widget.use24HourFormat) { + return TimeOfDay(hour: selectedHour, minute: selectedMinute); + } else { + int hour = selectedHour; + if (selectedPeriod == DayPeriod.pm && hour != 12) { + hour += 12; + } else if (selectedPeriod == DayPeriod.am && hour == 12) { + hour = 0; + } + return TimeOfDay(hour: hour, minute: selectedMinute); + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.dividerColor, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Select Time".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Utils.buildSvgWithAssets( + icon: AppAssets.cancel, + width: 24.h, + height: 24.h, + iconColor: AppColors.textColor, + ), + ), + ], + ), + ), + + // Time Picker + SizedBox( + height: 250.h, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Hour Picker + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: widget.use24HourFormat ? selectedHour : (selectedHour - 1), + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + if (widget.use24HourFormat) { + selectedHour = index; + } else { + selectedHour = index + 1; + } + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: List.generate( + widget.use24HourFormat ? 24 : 12, + (index) { + final hour = widget.use24HourFormat ? index : index + 1; + return Center( + child: Text( + hour.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ); + }, + ), + ), + ), + + // Separator + Text( + ':', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + + // Minute Picker + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: selectedMinute, + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + selectedMinute = index; + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: List.generate( + 60, + (index) => Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + ), + ), + ), + + // AM/PM Picker (only for 12-hour format) + if (!widget.use24HourFormat) + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: selectedPeriod == DayPeriod.am ? 0 : 1, + ), + itemExtent: 50.h, + onSelectedItemChanged: (index) { + setState(() { + selectedPeriod = index == 0 ? DayPeriod.am : DayPeriod.pm; + widget.onTimeChanged(_getCurrentTime()); + }); + }, + children: [ + Center( + child: Text( + 'AM', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + Center( + child: Text( + 'PM', + style: TextStyle( + fontSize: 24.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + if (widget.displaySelectedTime) + // Current Time Display + Container( + margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + padding: EdgeInsets.symmetric(vertical: 12.h), + decoration: BoxDecoration( + color: AppColors.lightGrayBGColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Center( + child: TimePickerWidget.formatTime( + _getCurrentTime(), + use24HourFormat: widget.use24HourFormat, + ).toText20( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ), + + // Action Buttons + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + child: Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: "Cancel".needTranslation, + onPressed: () => Navigator.pop(context), + textColor: AppColors.textColor, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + height: 56.h, + text: "Confirm".needTranslation, + onPressed: () { + Navigator.pop(context, _getCurrentTime()); + }, + textColor: AppColors.whiteColor, + backgroundColor: AppColors.primaryRedColor, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/time_picker_widget_usage_example.dart b/lib/widgets/time_picker_widget_usage_example.dart new file mode 100644 index 0000000..9ff6c35 --- /dev/null +++ b/lib/widgets/time_picker_widget_usage_example.dart @@ -0,0 +1,165 @@ +// Example usage of TimePickerWidget +// +// This file demonstrates how to use the TimePickerWidget in your Flutter app. +// The TimePickerWidget is a reusable component that shows a bottom sheet with +// an iOS-style time picker. + +// ============================================================================ +// EXAMPLE 1: Using with TextInputWidget +// ============================================================================ +/* +TextInputWidget( + labelText: "Appointment Time", + hintText: "Select time", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + print("Selected time: $value"); + }, +) +*/ + +// ============================================================================ +// EXAMPLE 2: Direct usage with custom button +// ============================================================================ +/* +ElevatedButton( + onPressed: () async { + final selectedTime = await TimePickerWidget.show( + context, + initialTime: TimeOfDay.now(), + use24HourFormat: false, + onTimeSelected: (time) { + print("Time selected: ${TimePickerWidget.formatTime(time)}"); + }, + ); + + if (selectedTime != null) { + print("Final time: ${TimePickerWidget.formatTime(selectedTime)}"); + } + }, + child: Text("Pick Time"), +) +*/ + +// ============================================================================ +// EXAMPLE 3: Using with 24-hour format +// ============================================================================ +/* +TextInputWidget( + labelText: "Meeting Time", + hintText: "Select time (24h)", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + // The value will be formatted as "14:30" for 2:30 PM in 24h format + print("Selected time (24h): $value"); + }, +) + +// Or programmatically: +final time = await TimePickerWidget.show( + context, + use24HourFormat: true, // Enable 24-hour format +); +*/ + +// ============================================================================ +// EXAMPLE 4: Parsing and formatting times +// ============================================================================ +/* +// Parse time string to TimeOfDay +String timeString = "02:30 PM"; +TimeOfDay? parsedTime = TimePickerWidget.parseTime(timeString); + +// Format TimeOfDay to string +TimeOfDay time = TimeOfDay(hour: 14, minute: 30); +String formatted12h = TimePickerWidget.formatTime(time); // "02:30 PM" +String formatted24h = TimePickerWidget.formatTime(time, use24HourFormat: true); // "14:30" +*/ + +// ============================================================================ +// EXAMPLE 5: Complete form example with date and time +// ============================================================================ +/* +class AppointmentForm extends StatefulWidget { + @override + _AppointmentFormState createState() => _AppointmentFormState(); +} + +class _AppointmentFormState extends State { + final TextEditingController dateController = TextEditingController(); + final TextEditingController timeController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Date picker + TextInputWidget( + labelText: "Appointment Date", + hintText: "Select date", + controller: dateController, + selectionType: SelectionTypeEnum.calendar, + isReadOnly: true, + onChange: (value) { + print("Date selected: $value"); + }, + ), + + SizedBox(height: 16), + + // Time picker + TextInputWidget( + labelText: "Appointment Time", + hintText: "Select time", + controller: timeController, + selectionType: SelectionTypeEnum.time, + isReadOnly: true, + onChange: (value) { + print("Time selected: $value"); + }, + ), + ], + ); + } +} +*/ + +// ============================================================================ +// Features: +// ============================================================================ +// ✅ iOS-style cupertino picker (works on both iOS and Android) +// ✅ Support for 12-hour format (with AM/PM) +// ✅ Support for 24-hour format +// ✅ Beautiful bottom sheet UI matching app design +// ✅ Real-time preview of selected time +// ✅ Confirm/Cancel buttons +// ✅ Easy integration with TextInputWidget +// ✅ Parse and format time utilities +// ✅ Fully customizable and reusable + +// ============================================================================ +// API Reference: +// ============================================================================ +// TimePickerWidget.show() - Shows the time picker bottom sheet +// Parameters: +// - context: BuildContext (required) +// - initialTime: TimeOfDay? (optional, defaults to current time) +// - use24HourFormat: bool (optional, defaults to false) +// - onTimeSelected: Function(TimeOfDay)? (optional callback) +// Returns: Future +// +// TimePickerWidget.formatTime() - Formats TimeOfDay to string +// Parameters: +// - time: TimeOfDay (required) +// - use24HourFormat: bool (optional, defaults to false) +// Returns: String (e.g., "02:30 PM" or "14:30") +// +// TimePickerWidget.parseTime() - Parses time string to TimeOfDay +// Parameters: +// - timeString: String (required, e.g., "02:30 PM") +// Returns: TimeOfDay? + diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..42b828d --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1985 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + url: "https://pub.dev" + source: hosted + version: "1.3.59" + amazon_payfort: + dependency: "direct main" + description: + name: amazon_payfort + sha256: "7732df0764aecbb814f910db36d0dca2f696e7e5ea380b49aa3ec62965768b33" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + barcode_scan2: + dependency: "direct main" + description: + name: barcode_scan2 + sha256: "0f3eb7c0a0c80a0f65d3fa88737544fdb6d27127a4fad566e980e626f3fb76e1" + url: "https://pub.dev" + source: hosted + version: "4.5.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carp_serializable: + dependency: transitive + description: + name: carp_serializable + sha256: f039f8ea22e9437aef13fe7e9743c3761c76d401288dcb702eadd273c3e4dcef + url: "https://pub.dev" + source: hosted + version: "2.0.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + chewie: + dependency: transitive + description: + name: chewie + sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca" + url: "https://pub.dev" + source: hosted + version: "1.13.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + device_calendar: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "5ea5ed9e2bb499c0633383b53103f2920b634755" + url: "https://github.com/bardram/device_calendar" + source: git + version: "4.3.1" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + sha256: c29b3e5147a82a06a4a08b3b574c51cb48cc17ad89893d53ee72a6f86643622e + url: "https://pub.dev" + source: hosted + version: "6.0.2" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f + url: "https://pub.dev" + source: hosted + version: "10.3.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: "4f85b161772e1d54a66893ef131c0a44bd9e552efa78b33d5f4f60d2caa5c8a3" + url: "https://pub.dev" + source: hosted + version: "11.6.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: a44b6d1155ed5cae7641e3de7163111cfd9f6f6c954ca916dc6a3bdfa86bf845 + url: "https://pub.dev" + source: hosted + version: "4.4.3" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: c7d1ed1f86ae64215757518af5576ff88341c8ce5741988c05cc3b2e07b0b273 + url: "https://pub.dev" + source: hosted + version: "0.5.10+16" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + url: "https://pub.dev" + source: hosted + version: "3.15.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5873a370f0d232918e23a5a6137dbe4c2c47cf017301f4ea02d9d636e52f60f0" + url: "https://pub.dev" + source: hosted + version: "6.0.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" + url: "https://pub.dev" + source: hosted + version: "15.2.10" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" + url: "https://pub.dev" + source: hosted + version: "4.6.10" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" + url: "https://pub.dev" + source: hosted + version: "3.10.10" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_ios_voip_kit_karmm: + dependency: "direct main" + description: + name: flutter_ios_voip_kit_karmm + sha256: "31a445d78aacacdf128a0354efb9f4e424285dfe4c0af3ea872e64f03e6f6bfc" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5" + url: "https://pub.dev" + source: hosted + version: "19.4.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_nfc_kit: + dependency: "direct main" + description: + name: flutter_nfc_kit + sha256: "3cc4059626fa672031261512299458dd274de4ccb57a7f0ee0951ddd70a048e5" + url: "https://pub.dev" + source: hosted + version: "3.6.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 + url: "https://pub.dev" + source: hosted + version: "2.0.30" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_staggered_animations: + dependency: "direct main" + description: + name: flutter_staggered_animations + sha256: "81d3c816c9bb0dca9e8a5d5454610e21ffb068aedb2bde49d2f8d04f75538351" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + flutter_swiper_view: + dependency: "direct main" + description: + name: flutter_swiper_view + sha256: "2a165b259e8a4c49d4da5626b967ed42a73dac2d075bd9e266ad8d23b9f01879" + url: "https://pub.dev" + source: hosted + version: "1.1.8" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_widget_from_html: + dependency: "direct main" + description: + name: flutter_widget_from_html + sha256: "7f1daefcd3009c43c7e7fb37501e6bb752d79aa7bfad0085fb0444da14e89bd0" + url: "https://pub.dev" + source: hosted + version: "0.17.1" + flutter_widget_from_html_core: + dependency: transitive + description: + name: flutter_widget_from_html_core + sha256: "1120ee6ed3509ceff2d55aa6c6cbc7b6b1291434422de2411b5a59364dd6ff03" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + flutter_zoom_videosdk: + dependency: "direct main" + description: + name: flutter_zoom_videosdk + sha256: "22731485fe48472a34ff0c7e787a382f5e1ec662fd89186e58e760974fc2a0cb" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://pub.dev" + source: hosted + version: "8.2.14" + fwfh_cached_network_image: + dependency: transitive + description: + name: fwfh_cached_network_image + sha256: "484cb5f8047f02cfac0654fca5832bfa91bb715fd7fc651c04eb7454187c4af8" + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_chewie: + dependency: transitive + description: + name: fwfh_chewie + sha256: ae74fc26798b0e74f3983f7b851e74c63b9eeb2d3015ecd4b829096b2c3f8818 + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_just_audio: + dependency: transitive + description: + name: fwfh_just_audio + sha256: dfd622a0dfe049ac647423a2a8afa7f057d9b2b93d92710b624e3d370b1ac69a + url: "https://pub.dev" + source: hosted + version: "0.17.0" + fwfh_svg: + dependency: transitive + description: + name: fwfh_svg + sha256: "2e6bb241179eeeb1a7941e05c8c923b05d332d36a9085233e7bf110ea7deb915" + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_url_launcher: + dependency: transitive + description: + name: fwfh_url_launcher + sha256: c38aa8fb373fda3a89b951fa260b539f623f6edb45eee7874cb8b492471af881 + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_webview: + dependency: transitive + description: + name: fwfh_webview + sha256: f71b0aa16e15d82f3c017f33560201ff5ae04e91e970cab5d12d3bcf970b870c + url: "https://pub.dev" + source: hosted + version: "0.15.6" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3 + url: "https://pub.dev" + source: hosted + version: "0.2.3" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b + url: "https://pub.dev" + source: hosted + version: "8.2.0" + gms_check: + dependency: "direct main" + description: + name: gms_check + sha256: b3fc08fd41da233f9761f9981303346aa9778b4802e90ce9bd8122674fcca6f0 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + google_api_availability: + dependency: "direct main" + description: + name: google_api_availability + sha256: "2ffdc91e1e0cf4e7974fef6c2988a24cefa81f03526ff04b694df6dc0fcbca03" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + google_api_availability_android: + dependency: transitive + description: + name: google_api_availability_android + sha256: "4794147f43a8f3eee6b514d3ae30dbe6f7b9048cae8cd2a74cb4055cd28d74a8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + google_api_availability_platform_interface: + dependency: transitive + description: + name: google_api_availability_platform_interface + sha256: "65b7da62fe5b582bb3d508628ad827d36d890710ea274766a992a56fa5420da6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" + url: "https://pub.dev" + source: hosted + version: "8.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: c389e16fafc04b37a4105e0757ecb9d59806026cee72f408f1ba68811d01bfe6 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: a6c9d43f6a944ff4bae5c3deb34817970ac3d591dcd7f5bd2ea450ab9e9c514a + url: "https://pub.dev" + source: hosted + version: "2.18.2" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: ca02463b19a9abc7d31fcaf22631d021d647107467f741b917a69fa26659fd75 + url: "https://pub.dev" + source: hosted + version: "2.15.5" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: f4b9b44f7b12a1f6707ffc79d082738e0b7e194bf728ee61d2b3cdf5fdf16081 + url: "https://pub.dev" + source: hosted + version: "2.14.0" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: "53e5dbf73ff04153acc55a038248706967c21d5b6ef6657a57fce2be73c2895a" + url: "https://pub.dev" + source: hosted + version: "0.5.14+2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + health: + dependency: "direct main" + description: + name: health + sha256: "320633022fb2423178baa66508001c4ca5aee5806ffa2c913e66488081e9fd47" + url: "https://pub.dev" + source: hosted + version: "13.1.4" + hijri_gregorian_calendar: + dependency: "direct main" + description: + name: hijri_gregorian_calendar + sha256: aecdbe3c9365fac55f17b5e1f24086a81999b1e5c9372cb08888bfbe61e07fa1 + url: "https://pub.dev" + source: hosted + version: "0.1.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: "direct main" + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + huawei_location: + dependency: "direct main" + description: + name: huawei_location + sha256: "3100d6b2b11df56481b8deade71baa84970e0bae0ade6ec56407be2b036af355" + url: "https://pub.dev" + source: hosted + version: "6.14.2+301" + huawei_map: + dependency: "direct main" + description: + path: flutter-hms-map + ref: HEAD + resolved-ref: "9a16541e4016e3bf58a2571e6aa658a4751af399" + url: "https://github.com/fleoparra/hms-flutter-plugin.git" + source: git + version: "6.11.2+303" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + jiffy: + dependency: "direct main" + description: + name: jiffy + sha256: "9bafbfe6d97587048bf449165e050029e716a12438f54a3d39e7e3a256decdac" + url: "https://pub.dev" + source: hosted + version: "6.4.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" + url: "https://pub.dev" + source: hosted + version: "0.10.5" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.dev" + source: hosted + version: "0.4.16" + keyboard_actions: + dependency: "direct main" + description: + name: keyboard_actions + sha256: "31e0ab2a706ac8f58887efa60efc1f19aecdf37d8ab0f665a0f156d1fbeab650" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf" + url: "https://pub.dev" + source: hosted + version: "1.0.53" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" + url: "https://pub.dev" + source: hosted + version: "1.6.1" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + location: + dependency: "direct main" + description: + name: location + sha256: b080053c181c7d152c43dd576eec6436c40e25f326933051c330da563ddd5333 + url: "https://pub.dev" + source: hosted + version: "8.0.1" + location_platform_interface: + dependency: transitive + description: + name: location_platform_interface + sha256: ca8700bb3f6b1e8b2afbd86bd78b2280d116c613ca7bfa1d4d7b64eba357d749 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + location_web: + dependency: transitive + description: + name: location_web + sha256: b8e3add5efe0d65c5e692b7a135d80a4015c580d3ea646fa71973e97668dd868 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" + url: "https://pub.dev" + source: hosted + version: "3.3.2" + manage_calendar_events: + dependency: "direct main" + description: + name: manage_calendar_events + sha256: f17600fcb7dc7047120c185993045e493d686930237b4e3c2689c26a64513d66 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + maps_launcher: + dependency: "direct main" + description: + name: maps_launcher + sha256: dac4c609720211fa6336b5903d917fe45e545c6b5665978efc3db2a3f436b1ae + url: "https://pub.dev" + source: hosted + version: "3.0.0+1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + ndef: + dependency: transitive + description: + name: ndef + sha256: "5083507cff4bb823b2a198a27ea2c70c4d6bc27a97b66097d966a250e1615d54" + url: "https://pub.dev" + source: hosted + version: "0.3.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 + url: "https://pub.dev" + source: hosted + version: "6.1.4" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rrule: + dependency: transitive + description: + name: rrule + sha256: b7425410c594d4b6717c9f17ec8ef83c9d1ff2e513c428a135b5924fc2e8e045 + url: "https://pub.dev" + source: hosted + version: "0.2.17" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 + url: "https://pub.dev" + source: hosted + version: "11.1.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + url: "https://pub.dev" + source: hosted + version: "2.4.13" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sizer: + dependency: "direct main" + description: + name: sizer + sha256: "9963c89e4d30d7c2108de3eafc0a7e6a4a8009799376ea6be5ef0a9ad87cfbad" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + smooth_corner: + dependency: "direct main" + description: + name: smooth_corner + sha256: "112d7331f82ead81ec870c5d1eb0624f2e7e367eccd166c2fffe4c11d4f87c4f" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + sms_otp_auto_verify: + dependency: "direct main" + description: + name: sms_otp_auto_verify + sha256: ee02af0d6b81d386ef70d7d0317a1929bc0b4a3a30a451284450bbcf6901ba1a + url: "https://pub.dev" + source: hosted + version: "2.2.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + syncfusion_flutter_calendar: + dependency: "direct main" + description: + name: syncfusion_flutter_calendar + sha256: "8e8a4eef01d6a82ae2c17e76d497ff289ded274de014c9f471ffabc12d1e2e71" + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0 + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_flutter_datepicker: + dependency: transitive + description: + name: syncfusion_flutter_datepicker + sha256: b5f35cc808e91b229d41613efe71dadab1549a35bfd493f922fc06ccc2fe908c + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_localizations: + dependency: transitive + description: + name: syncfusion_localizations + sha256: bb32b07879b4c1dee5d4c8ad1c57343a4fdae55d65a87f492727c11b68f23164 + url: "https://pub.dev" + source: hosted + version: "30.2.7" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b" + url: "https://pub.dev" + source: hosted + version: "6.3.22" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_player: + dependency: transitive + description: + name: video_player + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "6cfe0b1e102522eda1e139b82bf00602181c5844fd2885340f595fb213d74842" + url: "https://pub.dev" + source: hosted + version: "2.8.14" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd + url: "https://pub.dev" + source: hosted + version: "2.8.4" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a + url: "https://pub.dev" + source: hosted + version: "6.4.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + web: + dependency: "direct main" + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba + url: "https://pub.dev" + source: hosted + version: "4.13.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "21507ea5a326ceeba4d29dea19e37d92d53d9959cfc746317b9f9f7a57418d87" + url: "https://pub.dev" + source: hosted + version: "4.10.3" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fea63576b3b7e02b2df8b78ba92b48ed66caec2bb041e9a0b1cbd586d5d80bfd + url: "https://pub.dev" + source: hosted + version: "3.23.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index c4fa97b..461d3ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: # firebase_core: ^3.13.1 permission_handler: ^12.0.1 flutter_local_notifications: ^19.4.1 + timezone: ^0.10.0 provider: ^6.1.5+1 get_it: ^8.2.0 just_audio: ^0.10.4 @@ -86,6 +87,7 @@ dependencies: location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 + huawei_health: ^6.16.0+300 intl: ^0.20.2 flutter_widget_from_html: ^0.17.1 huawei_map: @@ -114,6 +116,7 @@ flutter: - assets/images/svg/ - assets/images/png/ - assets/images/png/zoom/ + - assets/images/png/smartwatches/ - assets/animations/ - assets/animations/lottie/