diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0ffb97db..2987d3b1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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 d423bc11..7832df8c 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 5c789c6f..a769c7a2 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 b657ac66..2926e9ad 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 4f7ef74c..8c1388b5 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 00000000..4df25bc9 --- /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 00000000..d0127998 --- /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 00000000..5bc332dc --- /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 00000000..adde1206 --- /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 00000000..5b9f19e6 --- /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 00000000..9856a49e --- /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 00000000..d8aea7bd --- /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 00000000..c07d1ded --- /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 00000000..18463d26 --- /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 00000000..b822d676 --- /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 00000000..6c7306d8 --- /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 f1daf69d..65bc4b37 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 328e8fc8..2d103337 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. - + pk.eyJ1IjoicndhaWQiLCJhIjoiY2x5cGo4aHNjMGNsbTJyc2djempobGQxaSJ9.RCaC6WrUt4A4YnZNfxnONQ diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 866b2708..9b679bc9 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 3e6502fc..6d0842d7 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,7 +18,7 @@ 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 00000000..db3a6133 Binary files /dev/null and b/assets/images/png/bmi_image_1.png differ diff --git a/assets/images/svg/add_icon_dark.svg b/assets/images/svg/add_icon_dark.svg new file mode 100644 index 00000000..399df3c7 --- /dev/null +++ b/assets/images/svg/add_icon_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/blood_pressure.svg b/assets/images/svg/blood_pressure.svg new file mode 100644 index 00000000..67badbe7 --- /dev/null +++ b/assets/images/svg/blood_pressure.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bmi_2.svg b/assets/images/svg/bmi_2.svg new file mode 100644 index 00000000..38468d70 --- /dev/null +++ b/assets/images/svg/bmi_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/cup_add.svg b/assets/images/svg/cup_add.svg new file mode 100644 index 00000000..ebe186a0 --- /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 00000000..fae08fe0 --- /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 00000000..6a085bb7 --- /dev/null +++ b/assets/images/svg/cup_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/dumbell_icon.svg b/assets/images/svg/dumbell_icon.svg new file mode 100644 index 00000000..1d6db5f3 --- /dev/null +++ b/assets/images/svg/dumbell_icon.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 00000000..1df8eec0 --- /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 00000000..7bb6fbbe --- /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 00000000..e0411917 --- /dev/null +++ b/assets/images/svg/green_tick_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/heart_rate.svg b/assets/images/svg/heart_rate.svg new file mode 100644 index 00000000..15c754ff --- /dev/null +++ b/assets/images/svg/heart_rate.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 00000000..a1c361a2 --- /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 00000000..78cefdc1 --- /dev/null +++ b/assets/images/svg/height_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/list_icon.svg b/assets/images/svg/list_icon.svg new file mode 100644 index 00000000..e68f20b6 --- /dev/null +++ b/assets/images/svg/list_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/minimize_icon.svg b/assets/images/svg/minimize_icon.svg new file mode 100644 index 00000000..b60a041d --- /dev/null +++ b/assets/images/svg/minimize_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/notification_icon_grey.svg b/assets/images/svg/notification_icon_grey.svg new file mode 100644 index 00000000..9e5e8d55 --- /dev/null +++ b/assets/images/svg/notification_icon_grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/outer_bubbles.svg b/assets/images/svg/outer_bubbles.svg new file mode 100644 index 00000000..cfe860d4 --- /dev/null +++ b/assets/images/svg/outer_bubbles.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/profile_icon.svg b/assets/images/svg/profile_icon.svg new file mode 100644 index 00000000..20dfb2ba --- /dev/null +++ b/assets/images/svg/profile_icon.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 00000000..7038793b --- /dev/null +++ b/assets/images/svg/resp_rate.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/symptom_bottom_icon.svg b/assets/images/svg/symptom_bottom_icon.svg new file mode 100644 index 00000000..bc729711 --- /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 00000000..e41c1ddf --- /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 00000000..14c7da4e --- /dev/null +++ b/assets/images/svg/temperature.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/water_bottle.svg b/assets/images/svg/water_bottle.svg new file mode 100644 index 00000000..4763d7ea --- /dev/null +++ b/assets/images/svg/water_bottle.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/svg/weight_2.svg b/assets/images/svg/weight_2.svg new file mode 100644 index 00000000..c22441fb --- /dev/null +++ b/assets/images/svg/weight_2.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 00000000..c3329ff0 --- /dev/null +++ b/assets/images/svg/weight_scale_icon.svg @@ -0,0 +1,3 @@ + + + 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 00000000..f2ca09fc --- /dev/null +++ b/assets/images/svg/yellow_arrow_down_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /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 00000000..4f91d052 --- /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 00000000..b487f033 --- /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 00000000..de67f9b9 --- /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 00000000..f1b3f098 --- /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 00000000..e703b64c --- /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 00000000..37687806 --- /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 00000000..c4a44243 --- /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 00000000..fd9fb401 --- /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 00000000..47454d3e --- /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 00000000..88530649 --- /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 00000000..4da39dc4 --- /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 00000000..e41979d6 --- /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 00000000..e7ce55b4 --- /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 00000000..029bec35 --- /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 00000000..b5161eb0 --- /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 00000000..a88bb5d0 --- /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 2eab03ad..7a41ae2c 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 6a5d34f1..64d7428e 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 00000000..4f91d052 --- /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 00000000..b487f033 --- /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 00000000..de67f9b9 --- /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 00000000..f1b3f098 --- /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 00000000..e703b64c --- /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 00000000..37687806 --- /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 00000000..db02e8f9 --- /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 00000000..fd9fb401 --- /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 00000000..47454d3e --- /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 00000000..88530649 --- /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 00000000..4da39dc4 --- /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/Penguin/PenguinModel.swift b/ios/Runner/Penguin/PenguinModel.swift new file mode 100644 index 00000000..7b6ab2d1 --- /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 00000000..31cf6262 --- /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 00000000..029bec35 --- /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 00000000..d5303e2e --- /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 00000000..a88bb5d0 --- /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/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements new file mode 100644 index 00000000..319178a4 --- /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 888f7040..f3663294 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}"); 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 b0fb2a83..f0a8a726 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -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'; @@ -437,14 +439,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'; @@ -670,25 +664,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 @@ -858,12 +833,11 @@ class ApiConsts { // SYMPTOMS CHECKER static final String getBodySymptomsByName = '$symptomsCheckerApi/GetBodySymptomsByName'; static final String getRiskFactors = '$symptomsCheckerApi/GetRiskFactors'; - static final String getGeneralSuggestion = '$symptomsCheckerApi/GetGeneralSggestion'; + static final String getSuggestions = '$symptomsCheckerApi/GetSuggestion'; static final String diagnosis = '$symptomsCheckerApi/diagnosis'; 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'; @@ -871,6 +845,14 @@ 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"; + // ************ static values for Api **************** static final double appVersionID = 50.3; static final int appChannelId = 3; @@ -882,3 +864,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 3c11b096..1a630906 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -246,7 +246,29 @@ 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'; + + // PNGS static const String bloodSugar = '$svgBasePath/bloodsugar.svg'; @@ -264,6 +286,19 @@ class AppAssets { 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'; @@ -289,7 +324,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'; } diff --git a/lib/core/cache_consts.dart b/lib/core/cache_consts.dart index bcbb1853..c1e06aa3 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 3f5065cb..f156ecb9 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 872fe69b..93100aa4 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'; @@ -31,11 +32,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_reports/monthly_reports_repo.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'; @@ -49,7 +53,8 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_r 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/presentation/health_calculators/health_calculator_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/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; @@ -59,6 +64,7 @@ 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'; @@ -112,6 +118,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()), @@ -137,6 +150,9 @@ 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(() => MonthlyReportRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), @@ -151,7 +167,6 @@ class AppDependencies { ), ); - // ViewModels // Global/shared VMs → LazySingleton @@ -161,25 +176,25 @@ 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( + () => 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(), + errorHandlerService: getIt() ), ); @@ -192,7 +207,12 @@ 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( @@ -206,8 +226,15 @@ 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()); @@ -220,7 +247,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( @@ -233,45 +267,55 @@ class AppDependencies { getIt.registerLazySingleton(() => HealthCalcualtorViewModel()); - getIt.registerLazySingleton( - () => TodoSectionViewModel(todoSectionRepo: getIt(), errorHandlerService: getIt()), + 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(), + ), ); - getIt.registerLazySingleton( - () => HealthProvider(), - ); + getIt.registerLazySingleton(() => HealthProvider()); + + getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt())); + + getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + + getIt.registerLazySingleton(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); getIt.registerLazySingleton( () => ActivePrescriptionsViewModel( - errorHandlerService: getIt(), - activePrescriptionsRepo: getIt() + errorHandlerService: getIt(), + activePrescriptionsRepo: getIt() ), ); getIt.registerFactory( () => QrParkingViewModel( qrParkingRepo: getIt(), errorHandlerService: getIt(), - cacheService: getIt(), + cacheService: getIt(), ), ); - - - // Screen-specific VMs → Factory - // getIt.registerFactory( - // () => BookAppointmentsViewModel( - // bookAppointmentsRepo: getIt(), - // dialogService: getIt(), - // errorHandlerService: getIt(), - // ), - // ); } } diff --git a/lib/core/location_util.dart b/lib/core/location_util.dart index 487b2282..9dcdbb5d 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 cf52306e..e13eb5cd 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 a42a44d0..746d2a7a 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/local_notifications.dart b/lib/core/utils/local_notifications.dart deleted file mode 100644 index aba01f85..00000000 --- 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 00000000..1f190376 --- /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 a96b8050..88e8cc84 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -15,16 +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: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: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 { @@ -36,7 +31,7 @@ Future backgroundMessageHandler(dynamic message) async { // showCallkitIncoming(message); _incomingCall(message.data); return; - } else {} + } } callPage(String sessionID, String token) async {} @@ -323,7 +318,7 @@ class PushNotificationHandler { if (fcmToken != null) onToken(fcmToken); // } } catch (ex) { - print("Notification Exception: " + ex.toString()); + print("Notification Exception: $ex"); } FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); } @@ -331,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( @@ -378,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); }); @@ -401,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; } @@ -427,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); } @@ -441,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_utils.dart b/lib/core/utils/size_utils.dart index 8a0703e6..4fdc09cf 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,16 @@ 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; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 38d04b96..03ff6b59 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, @@ -326,7 +370,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 +395,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 +766,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 +912,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 +940,6 @@ class Utils { launchUrl(uri, mode: LaunchMode.inAppBrowserView); } - static Color getCardBorderColor(int currentQueueStatus) { switch (currentQueueStatus) { case 0: diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 2039fb85..309dde19 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 4a1517c3..9b09c43c 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -260,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.loginType==1 || newRequest.loginType==4)) { - // 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 46649fb0..b3937727 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -32,6 +32,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie 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'; @@ -39,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'; @@ -565,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) { @@ -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/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index f6e83ff1..cfd473ee 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -5,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'; @@ -102,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 { @@ -1046,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 ae74ffb7..cbed940e 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,6 +45,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isDoctorsListLoading = false; bool isDoctorProfileLoading = false; bool isDoctorSearchByNameStarted = false; + bool isAppointmentNearestGateLoading = false; bool isLiveCareSchedule = false; bool isGetDocForHealthCal = false; @@ -132,6 +134,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { PatientAppointmentShareResponseModel? patientWalkInAppointmentShareResponseModel; + AppointmentNearestGateResponseModel? appointmentNearestGateResponseModel; + ///variables for laser clinic List femaleLaserCategory = [ LaserCategoryType(1, 'bodyString'), @@ -1343,4 +1347,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 00000000..bdaa4e23 --- /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/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart index b2b3709e..85e6018e 100644 --- a/lib/features/hmg_services/hmg_services_repo.dart +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -17,8 +17,11 @@ import 'package:hmg_patient_app_new/services/logger_service.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(); @@ -60,7 +63,11 @@ abstract class HmgServicesRepo { Future>>> searchEReferral(SearchEReferralRequestModel requestModel); + Future>>> getCovidTestProcedures(); + Future>> getCovidPaymentInfo(String procedureID, int projectID); + + Future>>> getPatientVitalSign(); } class HmgServicesRepoImp implements HmgServicesRepo { @@ -816,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 5da08609..c55a11c3 100644 --- a/lib/features/hmg_services/hmg_services_view_model.dart +++ b/lib/features/hmg_services/hmg_services_view_model.dart @@ -10,15 +10,18 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/req_models/crea 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'; @@ -35,6 +38,7 @@ class HmgServicesViewModel extends ChangeNotifier { bool isCmcServicesLoading = false; bool isUpdatingOrder = false; bool isHospitalListLoading = false; + bool isVitalSignLoading = false; // HHC specific loading states bool isHhcOrdersLoading = false; @@ -45,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 = []; @@ -60,7 +78,8 @@ class HmgServicesViewModel extends ChangeNotifier { List relationTypes = []; List getAllCitiesList = []; List searchReferralList = []; - + List covidTestProcedureList = []; + Covid19GetPaymentInfo? covidPaymentInfo; Future getOrdersList() async {} @@ -783,4 +802,117 @@ class HmgServicesViewModel extends ChangeNotifier { 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 00000000..c27f1e78 --- /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 00000000..5f95263b --- /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 00000000..bc93f59a --- /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/vital_sign_ui_model.dart b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart new file mode 100644 index 00000000..45b0ab65 --- /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 00000000..008f5719 --- /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 00000000..dd9531f2 --- /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/monthly_report/monthly_report_repo.dart b/lib/features/monthly_report/monthly_report_repo.dart new file mode 100644 index 00000000..429700c4 --- /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 00000000..348ca926 --- /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_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index f51f7017..c5dcaf62 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -1,6 +1,10 @@ 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'; @@ -30,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}); @@ -40,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(); @@ -71,7 +111,7 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { setSelectedRegionId(null); break; case AppointmentViaRegionState.HOSPITAL_SELECTION: - setBottomSheetState(AppointmentViaRegionState.TYPE_SELECTION); + setBottomSheetState(AppointmentViaRegionState.REGION_SELECTION); break; default: } @@ -129,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/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index b4a25eec..9bd48ec9 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -127,6 +127,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { initAppointmentsViewModel() { if (isAppointmentDataToBeLoaded) { + // Default view is grouped by clinic on first open. + isAppointmentsSortByClinic = true; patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); @@ -269,6 +271,9 @@ 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); @@ -280,6 +285,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { print('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); print('All Appointments: ${patientAppointmentsHistoryList.length}'); getFiltersForSelectedAppointmentList(filteredAppointmentList); + notifyListeners(); } void getFiltersForSelectedAppointmentList(List filteredAppointmentList) { 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 00000000..ef88623d --- /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 00000000..e8056d91 --- /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 00000000..68eee6e5 --- /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 00000000..a02d741d --- /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/prescriptions/prescriptions_repo.dart b/lib/features/prescriptions/prescriptions_repo.dart index e7a4f078..c6e7150b 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/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 00000000..b2be4a27 --- /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/symptoms_checker_repo.dart b/lib/features/symptoms_checker/symptoms_checker_repo.dart index 53792071..954d414a 100644 --- a/lib/features/symptoms_checker/symptoms_checker_repo.dart +++ b/lib/features/symptoms_checker/symptoms_checker_repo.dart @@ -7,77 +7,214 @@ 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/services/logger_service.dart'; -import 'package:http/http.dart' as http; abstract class SymptomsCheckerRepo { Future>> getBodySymptomsByName({ required List organNames, }); + + Future>> getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }); + + Future>> getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }); } 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>> getBodySymptomsByName({ - required List organNames, + Future>> getBodySymptomsByName({required List organNames}) async { + log("GetBodySymptomsByName Request URL: ${ApiConsts.getBodySymptomsByName}"); + log("GetBodySymptomsByName Request Body: ${jsonEncode(organNames)}"); + + Map headers = {'Content-Type': 'application/json', 'Accept': 'text/plain'}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getBodySymptomsByName, + apiHeaders: headers, + body: jsonEncode(organNames), + isExternal: true, + isAllowAny: true, + isBodyPlainText: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + loggerService.logError("GetBodySymptomsByName API Failed: $error"); + log("GetBodySymptomsByName Failed: $error, Status: $statusCode"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("GetBodySymptomsByName Response Status: $statusCode"); + loggerService.logInfo("GetBodySymptomsByName API Success: $response"); + log("GetBodySymptomsByName Response: $response"); + + 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"); + log("Parse Error: $e"); + 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"); + log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRiskFactors({ + required int age, + required String sex, + required List evidenceIds, + required String language, }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + }; + 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.getRiskFactors, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + log("GetRiskFactors Failed: $error, Status: $statusCode"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("GetRiskFactors Response: $response"); + + // 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"); + log("Parse Error: $e"); + 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 getRiskFactors: $e"); + loggerService.logError("StackTrace: $stackTrace"); + log("Exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getSuggestions({ + required int age, + required String sex, + required List evidenceIds, + required String language, + }) async { + final Map body = { + "age": { + "value": age, + }, + "sex": sex, + "evidence": evidenceIds.map((id) => {"id": id}).toList(), + "language": language, + }; - log("GetBodySymptomsByName Response Status: $statusCode"); - loggerService.logInfo("GetBodySymptomsByName Response Status: $statusCode"); + try { + GenericApiModel? apiResponse; + Failure? failure; - try { - // Parse the response - final responseBody = jsonDecode(response.body); + await apiClient.post( + ApiConsts.getSuggestions, + body: body, + isExternal: true, + isAllowAny: true, + onFailure: (error, statusCode, {messageStatus, failureType}) { + log("getSuggestions Failed: $error, Status: $statusCode"); + failure = failureType ?? ServerFailure(error); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + log("getSuggestions Response: $response"); - loggerService.logInfo("GetBodySymptomsByName API Success: $responseBody"); - log("GetBodySymptomsByName Response: $responseBody"); + // 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"); + log("Parse Error: $e"); + 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 fe66cf74..da439c6c 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -1,20 +1,24 @@ 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/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,9 +33,19 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // API loading states bool isBodySymptomsLoading = false; + bool isRiskFactorsLoading = false; + bool isSuggestionsLoading = false; // API data storage - using API models directly BodySymptomResponseModel? bodySymptomResponse; + RiskAndSuggestionsResponseModel? riskFactorsResponse; + RiskAndSuggestionsResponseModel? suggestionsResponse; + + // Selected risk factors tracking + final Set _selectedRiskFactorIds = {}; + + // Selected Suggestions tracking + final Set _selectedSuggestionsIds = {}; // Selected symptoms tracking (organId -> Set of symptom IDs) final Map> _selectedSymptomsByOrgan = {}; @@ -111,6 +125,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier { 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; notifyListeners(); @@ -122,6 +158,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 +171,11 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Show tooltip _showTooltip(organId); + if (_selectedOrganIds.isEmpty) { + _isBottomSheetExpanded = false; + notifyListeners(); + } + notifyListeners(); } @@ -157,6 +202,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier { void removeOrgan(String organId) { _selectedOrganIds.remove(organId); notifyListeners(); + if (_selectedOrganIds.isEmpty) { + _isBottomSheetExpanded = false; + notifyListeners(); + } } void clearAllSelections() { @@ -164,7 +213,12 @@ class SymptomsCheckerViewModel extends ChangeNotifier { notifyListeners(); } - void toggleBottomSheet() { + void toggleBottomSheet({bool? value}) { + if (value != null) { + _isBottomSheetExpanded = value; + notifyListeners(); + return; + } _isBottomSheetExpanded = !_isBottomSheetExpanded; notifyListeners(); } @@ -268,11 +322,305 @@ 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, + 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 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, + ); + + 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(); + } + + /// 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, + ); + + 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'); + } + } + }, + ); + } + void reset() { _currentView = BodyView.front; _selectedOrganIds.clear(); _selectedSymptomsByOrgan.clear(); + _selectedRiskFactorIds.clear(); + _selectedSuggestionsIds.clear(); bodySymptomResponse = null; + riskFactorsResponse = null; + suggestionsResponse = null; _isBottomSheetExpanded = false; _tooltipTimer?.cancel(); _tooltipOrganId = null; 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 00000000..a0af649a --- /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 00000000..01dbd922 --- /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 00000000..33dbbb20 --- /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 00000000..667653e0 --- /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 00000000..b86fae47 --- /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 00000000..14a74a7e --- /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 00000000..01167fef --- /dev/null +++ b/lib/features/water_monitor/water_monitor_view_model.dart @@ -0,0 +1,1340 @@ +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/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; + + WaterMonitorViewModel({required this.waterMonitorRepo}); + + // 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) { + log('Error fetching user progress: ${failure.message}'); + }, (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) { + _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.waterMonitorSettingsScreen); + } 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) { + _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) { + log('Error inserting user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + log("Insert 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 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 + 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) { + log('Error undoing user activity: ${failure.message}'); + _isLoading = false; + notifyListeners(); + return false; + }, + (apiModel) { + 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'); + } + } + } + } + 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/main.dart b/lib/main.dart index b672e4ef..17395835 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,16 +23,19 @@ 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/smartwatch_health_data/health_provider.dart'; -import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.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/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; @@ -73,7 +76,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(); @@ -168,6 +171,15 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), ), @@ -195,11 +207,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, diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 7b605b9f..bd815849 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -4,6 +4,8 @@ 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'; @@ -31,6 +33,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/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'; @@ -132,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, @@ -155,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(); @@ -164,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( diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index ccd70180..475ee702 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 4554873c..124bf254 100644 --- a/lib/presentation/appointments/appointment_queue_page.dart +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -42,8 +42,11 @@ class AppointmentQueuePage extends StatelessWidget { color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false, - side: - BorderSide(color: myAppointmentsVM.isAppointmentQueueDetailsLoading ? AppColors.whiteColor : 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), @@ -62,17 +65,23 @@ class AppointmentQueuePage extends StatelessWidget { ], ).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 10.h), - "Hala ${appState!.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + "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) .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 8.h), - myAppointmentsVM.currentPatientQueueDetails.queueNo!.toText32(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), + myAppointmentsVM.currentPatientQueueDetails.queueNo! + .toText32(isBold: true) + .toShimmer2(isShow: myAppointmentsVM.isAppointmentQueueDetailsLoading), SizedBox(height: 8.h), CustomButton( - text: Utils.getCardButtonText(myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), + text: Utils.getCardButtonText( + myAppointmentsVM.currentQueueStatus, myAppointmentsVM.currentPatientQueueDetails.roomNo ?? ""), onPressed: () {}, backgroundColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus), borderColor: Utils.getCardButtonColor(myAppointmentsVM.currentQueueStatus).withValues(alpha: 0.01), @@ -94,61 +103,72 @@ class AppointmentQueuePage extends StatelessWidget { ? 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), + 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, + 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), + 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, + 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), + ], + ), + ], + ); + }, + 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( @@ -171,15 +191,25 @@ 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), ], diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index abf2464c..b4c36305 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -4,9 +4,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/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'; @@ -22,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'; @@ -187,18 +185,9 @@ class _MyAppointmentsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - CustomButton( - text: "${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments", - 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, - ), + AppCustomChipWidget( + labelText: + "${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments"), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), @@ -224,7 +213,7 @@ class _MyAppointmentsPageState extends State { child: isExpanded ? Container( key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -232,63 +221,6 @@ class _MyAppointmentsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.network( - appointment.doctorImageURL!, - width: 24.h, - height: 24.h, - fit: BoxFit.fill, - ).circle(100), - SizedBox(width: 8.h), - Expanded(child: appointment.doctorNameObj!.toText14(weight: FontWeight.w500)), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - CustomButton( - text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointment.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: myAppointmentsVM.isAppointmentsSortByClinic ? appointment.projectName! : appointment.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: appointment.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), AppointmentCard( patientAppointmentHistoryResponseModel: appointment, myAppointmentsViewModel: myAppointmentsViewModel, @@ -296,9 +228,9 @@ class _MyAppointmentsPageState extends State { isLoading: false, isFromHomePage: false, ), - SizedBox(height: 12.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), - SizedBox(height: 12.h), + SizedBox(height: 8.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h).paddingSymmetrical(16.w, 0.h), + SizedBox(height: 8.h), ], ); }), diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 3740a4ad..2c5d1b0d 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -1,8 +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/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'; @@ -43,8 +41,8 @@ class _MyDoctorsPageState extends State { bookAppointmentsViewModel = Provider.of(context, listen: false); return CollapsingListView( - title: LocaleKeys.myDoctor.tr(context: context), - child: Consumer(builder: (context, myAppointmentsVM, child) { + title: LocaleKeys.myDoctor.tr(context: context), + child: Consumer(builder: (context, myAppointmentsVM, child) { // build grouped lists from the flat list final clinicMap = >{}; final hospitalMap = >{}; @@ -171,129 +169,121 @@ class _MyDoctorsPageState extends State { final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); final isExpanded = expandedIndex == index; - return Container( - key: _groupKeys.putIfAbsent(index, () => GlobalKey()), - margin: EdgeInsets.only(bottom: 12.h), - padding: EdgeInsets.all(16.h), + 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: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - final key = _groupKeys[index]; - if (key != null && key.currentContext != null && expandedIndex == index) { + 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: Duration(milliseconds: 350), + duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, - alignment: 0.1, + alignment: 0.0, ); } }); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomButton( - text: "${group.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.chevron_right, color: AppColors.greyColor), - ], - ), - SizedBox(height: 8.h), - Text( - displayName, - style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), - overflow: TextOverflow.ellipsis, - ), - ], + } + }); + }, + 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: Duration(milliseconds: 400), - child: isExpanded - ? Container( - key: ValueKey(index), - padding: EdgeInsets.only(top: 12.h), - child: Column( - children: group.map((doctor) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( + 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( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ Image.network( (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"), - width: 24.h, + width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500), - SizedBox(height: 6.h), - ], - ), + child: (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500), ), ], ), SizedBox(height: 8.h), - Row( + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, children: [ - CustomButton( - text: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.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, + AppCustomChipWidget( + labelText: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""), ), ], ), - SizedBox(height: 8.h), + SizedBox(height: 12.h), Row( children: [ Expanded( - flex: 6, + 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( @@ -320,75 +310,31 @@ class _MyDoctorsPageState extends State { ); }); }, - backgroundColor: AppColors.bgRedLightColor, - borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ), - SizedBox(width: 8.h), - 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: getIt().isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.whiteColor, - fit: BoxFit.contain, - ), - ), - ), - ).onPress(() 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, - ); - }); - }), - ), ], ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), ], - ), - ), - ); - }).toList(), - ), - ) - : SizedBox.shrink(), - ), - ], + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ), ), ); }, diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 57e19ca2..39e4e03d 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -179,7 +179,13 @@ 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, @@ -352,14 +358,14 @@ class AppointmentCard extends StatelessWidget { backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, textColor: AppColors.blackColor, - fontSize: 12.f, + fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 40.h, icon: AppAssets.rebook_appointment_icon, iconColor: AppColors.blackColor, - iconSize: 14.h, + iconSize: 16.h, ); } @@ -372,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/hospital_bottom_sheet/hospital_bottom_sheet_body.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart index ad48a6de..eed45dfb 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/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index 2def72cf..6588c2ea 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -79,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 93b26c71..f0fd8cb8 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/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 5aa1b7c5..76dc1be8 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}); @@ -511,7 +512,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(); diff --git a/lib/presentation/book_appointment/laser/laser_appointment.dart b/lib/presentation/book_appointment/laser/laser_appointment.dart index 28f0f2d0..19a38009 100644 --- a/lib/presentation/book_appointment/laser/laser_appointment.dart +++ b/lib/presentation/book_appointment/laser/laser_appointment.dart @@ -1,6 +1,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' 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'; @@ -81,7 +83,7 @@ class LaserAppointment extends StatelessWidget { activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null,LocaleKeys.malE.tr()), - CustomTabBarModel(null,"Female".needTranslation.tr()), + CustomTabBarModel(null, "Female".needTranslation), ], onTabChange: (index) { var viewmodel = context.read(); diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 40abd404..72658bfd 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -50,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/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index 450e86b5..48c0cd5c 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1019,12 +1019,12 @@ class _SelectClinicPageState extends State { regionalViewModel.setBottomSheetType(type); // AppointmentViaRegionViewmodel? viewmodel = null; showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, - child: Consumer(builder: (_, data, __) { - return getRegionalSelectionWidget(data); + 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(); } @@ -1035,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 diff --git a/lib/presentation/book_appointment/select_livecare_clinic_page.dart b/lib/presentation/book_appointment/select_livecare_clinic_page.dart index 23626173..502e38d3 100644 --- a/lib/presentation/book_appointment/select_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/select_livecare_clinic_page.dart @@ -48,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( 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 8a04843d..4a2a3049 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 @@ -142,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 b2666b69..3a9f57dd 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -185,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/covid19test/covid_19_questionnaire.dart b/lib/presentation/covid19test/covid_19_questionnaire.dart index 8d7e8bdd..8608d80c 100644 --- a/lib/presentation/covid19test/covid_19_questionnaire.dart +++ b/lib/presentation/covid19test/covid_19_questionnaire.dart @@ -7,10 +7,13 @@ 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'; @@ -46,87 +49,104 @@ class _Covid19QuestionnaireState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - body: Column(children: [ - Expanded( - child: CollapsingListView( + return CollapsingListView( title: "COVID-19", - child: Padding( + 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: [ - Expanded( - 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: [ - "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; + 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), - ), - ], - ); - }, - ), - ], + 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 - CustomButton( - text: "Next".needTranslation, - onPressed: () { - // Handle next action - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - fontSize: 16.f, - fontWeight: FontWeight.w600, - borderRadius: 12.r, - height: 56.h, - ), + ], ), + ), + ), - ), - ), - ])); + ); + + + } + 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 00000000..42e7736e --- /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 00000000..7ccbb02f --- /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/new_e_referral.dart b/lib/presentation/e_referral/new_e_referral.dart index b28b8dfe..3083de13 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/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index a3d5d76c..6df5a4a5 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -6,15 +6,19 @@ 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'; @@ -48,9 +52,21 @@ class ServicesPage extends StatelessWidget { "".needTranslation, AppAssets.emergency_services_icon, bgColor: AppColors.primaryRedColor, - true, - route: AppRoutes.eReferralPage, - ), + 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, @@ -58,7 +74,7 @@ class ServicesPage extends StatelessWidget { AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, - route: AppRoutes.eReferralPage, + route: AppRoutes.bookAppointmentPage, ), HmgServicesComponentModel( 5, @@ -69,6 +85,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, @@ -88,18 +114,40 @@ class ServicesPage extends StatelessWidget { 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( + // 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, @@ -133,7 +181,27 @@ class ServicesPage extends StatelessWidget { AppAssets.daily_water_monitor_icon, bgColor: AppColors.whiteColor, true, - route: AppRoutes.eReferralPage, + 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.waterMonitorSettingsScreen); + } else { + context.navigateWithName(AppRoutes.waterConsumptionScreen); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + context.navigateWithName(AppRoutes.waterConsumptionScreen); + }, + ); + }, ), HmgServicesComponentModel( 11, @@ -376,7 +444,7 @@ class ServicesPage extends StatelessWidget { SizedBox(height: 16.h), GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount:(isFoldable || isTablet) ? 6 : 4, // 4 icons per row + crossAxisCount: (isFoldable || isTablet) ? 6 : 4, // 4 icons per row crossAxisSpacing: 12.w, mainAxisSpacing: 18.h, childAspectRatio: 0.8, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index f51c10eb..c8e77601 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'; @@ -13,6 +14,7 @@ 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'; @@ -25,6 +27,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_ 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'; @@ -43,6 +46,9 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dar 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'; @@ -69,13 +75,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(); @@ -84,10 +90,6 @@ class _LandingPageState extends State { authVM = context.read(); habibWalletVM = context.read(); appointmentRatingViewModel = context.read(); - // myAppointmentsViewModel = context.read(); - // prescriptionsViewModel = context.read(); - // insuranceViewModel = context.read(); - // immediateLiveCareViewModel = context.read(); authVM.savePushTokenToAppState(); if (mounted) { @@ -99,13 +101,11 @@ class _LandingPageState extends State { if (appState.isAuthenticated) { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); + todoSectionViewModel.initializeTodoSectionViewModel(); immediateLiveCareViewModel.initImmediateLiveCare(); immediateLiveCareViewModel.getPatientLiveCareHistory(); myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false); - myAppointmentsViewModel.getPatientMyDoctors(); - prescriptionsViewModel.initPrescriptionsViewModel(); - insuranceViewModel.initInsuranceProvider(); emergencyServicesViewModel.checkPatientERAdvanceBalance(); myAppointmentsViewModel.getPatientAppointmentQueueDetails(); if(!appState.isRatedVisible) { @@ -113,17 +113,16 @@ class _LandingPageState extends State { 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); - - })); - - } - }); + }), + ); + } + }, + ); } } }); @@ -134,10 +133,10 @@ class _LandingPageState extends State { 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, @@ -195,7 +194,7 @@ class _LandingPageState extends State { ), ); }), - Utils.buildSvgWithAssets(icon: AppAssets.search_icon, height: 18.h, width: 18.h).onPress(() { + Utils.buildSvgWithAssets(icon: AppAssets.indoor_nav_icon, height: 18.h, width: 18.h).onPress(() { // Navigator.of(context).push( // CustomPageRoute( // page: MedicalFilePage(), @@ -216,6 +215,50 @@ class _LandingPageState extends State { ), ], ).paddingSymmetrical(24.h, 0.h), + !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: [ @@ -236,8 +279,8 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); }), SizedBox(height: 16.h), - Consumer2( - builder: (context, myAppointmentsVM, immediateLiveCareVM, child) { + Consumer3( + builder: (context, myAppointmentsVM, immediateLiveCareVM, todoSectionVM, child) { return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -454,20 +497,41 @@ class _LandingPageState extends State { SizedBox(height: 12.h), ], ) - : 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, - ), + : (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, + ), ); }, ) diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index 6ec8c3af..43cc3b90 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -1,15 +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}); @@ -33,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(), ], ), @@ -46,6 +51,20 @@ class _LandingNavigationState extends State { 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/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index 76ed3f76..62168808 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -1,19 +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: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 { @@ -117,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_health_care/hhc_procedures_page.dart b/lib/presentation/home_health_care/hhc_procedures_page.dart index 41d8a2f3..be97a886 100644 --- a/lib/presentation/home_health_care/hhc_procedures_page.dart +++ b/lib/presentation/home_health_care/hhc_procedures_page.dart @@ -446,68 +446,62 @@ class _HhcProceduresPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: Column( - children: [ - Expanded( - child: CollapsingListView( - title: "Home Health Care".needTranslation, - history: () => Navigator.of(context).push(CustomPageRoute(page: HhcOrderDetailPage(), direction: AxisDirection.up)), - 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, - ), - ), - ], - ); - } - }, - ), - ), - ), - 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, + 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, + ), + 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), ), - child: Padding( - padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 24.h, top: 24.h), - 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, ), ), - ); - } - - return SizedBox.shrink(); - }, - ), - ], + ], + ); + } + }, + ), ), ); } diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 150b15c5..415d66fe 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/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 3d774d36..fde58119 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/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index c131275d..90651f13 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/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/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: [ CustomButton( text: "${group.length} ${'results'.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), 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.h, vertical: 8.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.h, 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), 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: 8.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 +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/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 80a45ac3..fc92a810 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -3,9 +3,11 @@ 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/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_config.dart'; @@ -16,13 +18,18 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.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/active_medication/active_medication_page.dart'; @@ -47,9 +54,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'; @@ -82,25 +94,54 @@ 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(); late ActivePrescriptionsViewModel activePrescriptionsViewModel; 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); @@ -108,10 +149,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( @@ -185,8 +230,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, @@ -255,6 +303,111 @@ 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, @@ -273,7 +426,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( @@ -490,47 +643,51 @@ class _MedicalFilePageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: MedicalFileAppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - onRescheduleTap: () { - openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); - }, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: MedicalFileAppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], + myAppointmentsViewModel: myAppointmentsViewModel, + onRescheduleTap: () { + openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); + }, 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 { - print("Doctor is not available"); - } - }); + 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), ), @@ -648,64 +805,58 @@ class _MedicalFilePageState extends State { ); }), ), + )); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 16.h), + const Divider(color: AppColors.dividerColor), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "All Prescriptions".needTranslation, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionsListPage(), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + height: 40.h, + icon: AppAssets.requests, + iconColor: AppColors.primaryRedColor, + iconSize: 16.w, + ), ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), - SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: "All Prescriptions".needTranslation, - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionsListPage(), - ), - ); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - height: 40.h, - icon: AppAssets.requests, - iconColor: AppColors.primaryRedColor, - iconSize: 16.w, - ), - ), - SizedBox(width: 6.w), - Expanded( - child: CustomButton( - text: "All Medications".needTranslation, - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: ActiveMedicationPage(), - ), - ); - }, + SizedBox(width: 6.w), + Expanded( + child: CustomButton( + text: "All Medications".needTranslation, + ), + backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.h, - height: 40.h, - icon: AppAssets.all_medications_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.h, + height: 40.h, + icon: AppAssets.all_medications_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ), ), - ), - ], - ), - ], + ], + ), + ], ), ), ).paddingSymmetrical(0.w, 0.h) @@ -837,8 +988,7 @@ class _MedicalFilePageState extends State { }); }), ), - ), - ); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), @@ -1006,7 +1156,13 @@ class _MedicalFilePageState extends State { 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, @@ -1014,7 +1170,13 @@ class _MedicalFilePageState extends State { 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), @@ -1070,7 +1232,12 @@ class _MedicalFilePageState extends State { isLargeText: true, iconSize: 36.h, ).onPress(() { - Navigator.pushNamed(context, AppRoutes.monthlyReports); + monthlyReportViewModel.setHealthSummaryEnabled(cacheService.getBool(key: CacheConst.isMonthlyReportEnabled) ?? false); + Navigator.of(context).push( + CustomPageRoute( + page: MonthlyReport(), + ), + ); }), MedicalFileCard( label: "Medical Reports".needTranslation, @@ -1078,7 +1245,7 @@ class _MedicalFilePageState extends State { backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.medical_reports_icon, isLargeText: true, - iconSize: 36.h, + iconSize: 36.w, ).onPress(() { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.getPatientMedicalReportList(); @@ -1198,4 +1365,265 @@ class _MedicalFilePageState extends State { 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_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index cb784796..f6d7576f 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -84,7 +84,7 @@ class _MedicalReportsPageState extends State { // ), // ], // ).paddingSymmetrical(24.h, 0.h), - SizedBox(height: 20.h), + SizedBox(height: 8.h), Row( children: [ CustomButton( @@ -162,7 +162,7 @@ class _MedicalReportsPageState extends State { patientMedicalReportResponseModel: PatientMedicalReportResponseModel(), medicalFileViewModel: medicalFileVM, isLoading: true, - ).paddingSymmetrical(24.h, 8.h) + ).paddingSymmetrical(0.h, 8.h) : medicalFileViewModel.patientMedicalReportList.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, diff --git a/lib/presentation/monthly_report/monthly_report.dart b/lib/presentation/monthly_report/monthly_report.dart new file mode 100644 index 00000000..17765104 --- /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_invoices/my_invoices_details_page.dart b/lib/presentation/my_invoices/my_invoices_details_page.dart new file mode 100644 index 00000000..cccd671b --- /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 00000000..ef1a9c26 --- /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 00000000..27ca79a2 --- /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 265559b6..a40a27b4 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/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index a38ff924..1216c619 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 100d9638..a2a3e7fb 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 2cf2fcce..8b60159c 100644 --- a/lib/presentation/prescriptions/prescriptions_list_page.dart +++ b/lib/presentation/prescriptions/prescriptions_list_page.dart @@ -259,14 +259,14 @@ 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, + iconSize: 16.h, ), ), SizedBox(width: 8.h), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 9e4c8088..e463ae78 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 c0eb782e..fb153ead 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -190,231 +190,173 @@ class _RadiologyOrdersPageState extends State { 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: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Group header container with key so we can scroll to it - GestureDetector( - onTap: () { - setState(() { - expandedIndex = expandedIndex == index ? null : index; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); - if (key.currentContext != null && expandedIndex == index) { - // Delay scrolling to wait for expansion animation - Future.delayed(Duration(milliseconds: 450), () { - if (key.currentContext != null) { - Scrollable.ensureVisible( - key.currentContext!, - duration: Duration(milliseconds: 350), - curve: Curves.easeInOut, - alignment: 0.0, - ); - } - }); - } - }); - }, - child: Container( - key: _groupKeys.putIfAbsent(index, () => GlobalKey()), - margin: EdgeInsets.only(bottom: 8.h), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.h), - boxShadow: [ - BoxShadow( - color: AppColors.blackColor.withValues(alpha: 0.03), - blurRadius: 6, - offset: Offset(0, 2), - ) - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + 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: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomButton( - text: "${group.length} ${'results'.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(expandedIndex == index ? Icons.expand_less : Icons.expand_more), - ], - ), - SizedBox(height: 8.h), - Text( - displayName, - style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), - overflow: TextOverflow.ellipsis, - ), + AppCustomChipWidget(labelText: "${group.length} ${'results'.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: Duration(milliseconds: 400), - child: expandedIndex == index - ? Container( - key: ValueKey(index), - padding: EdgeInsets.only(top: 12.h), - child: Column( - children: group.map((order) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.network( - order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", - width: 24.h, - height: 24.h, - fit: BoxFit.cover, - ).circle(100), - SizedBox(width: 8.h), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - (order.doctorName ?? "").toString().toText14(weight: FontWeight.w500), - SizedBox(height: 6.h), - - - ], + 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: [ + 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), - Row(children: [ - CustomButton( - text: order.description!, - 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: 6.h), - - Row( - children: [ - CustomButton( - text: DateUtil.formatDateToDate(order.orderDate ?? order.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 ? (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: 8.h), - Row( - children: [ - Expanded( - flex: 6, - child: SizedBox(), - ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( - height: 40.h, - width: 40.w, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, - borderRadius: 12, + ], + ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + if ((order.description ?? '').isNotEmpty) + AppCustomChipWidget( + labelText: (order.description ?? '').toString(), ), - child: Padding( - padding: EdgeInsets.all(12.h), - child: Transform.flip( - flipX: false, - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.whiteColor, - fit: BoxFit.contain, - ), - ), + AppCustomChipWidget( + labelText: DateUtil.formatDateToDate( + (order.orderDate ?? order.appointmentDate), + false, ), - ).onPress(() { - model.navigationService.push( - CustomPageRoute( - page: RadiologyResultPage(patientRadiologyResponseModel: order), - ), - ); - }), - ), - ], - ), - ], - ), - ), - ); - }).toList(), - ), - ) - : SizedBox.shrink(), + ), + AppCustomChipWidget( + labelText: model.isSortByClinic ? (order.clinicDescription ?? '') : (order.projectName ?? ''), + ), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + 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), + ], + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], ), - ], + ), ), ), ), diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index d5dc32c2..c23b956f 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -267,14 +267,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/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 29925935..d4ff2cb5 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -1,16 +1,15 @@ -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/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/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 { @@ -24,23 +23,24 @@ class _RiskFactorsScreenState extends State { @override void initState() { super.initState(); - // Initialize symptom groups based on selected organs + // 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 + if (viewModel.hasSelectedRiskFactors) { context.navigateWithName(AppRoutes.suggestionsScreen); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Please select at least one option before proceeding'.needTranslation), + content: Text('Please select at least one risk before proceeding'.needTranslation), backgroundColor: AppColors.errorColor, ), ); @@ -51,27 +51,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 +69,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 +214,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 +247,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 +256,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 28325150..f2aa71ec 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -1,16 +1,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/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/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 { @@ -27,14 +25,18 @@ class _SuggestionsScreenState extends State { // 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); } else { @@ -51,27 +53,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 +71,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 +191,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/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index aa0cd72f..ff0482e1 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -148,10 +148,7 @@ class _TriageScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - currentQuestion.question, - style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.w500, color: AppColors.textColor), - ), + currentQuestion.question.toText16(weight: FontWeight.w500), SizedBox(height: 24.h), ...List.generate(currentQuestion.options.length, (index) { bool selected = currentQuestion.selectedOptionIndex == index; @@ -179,17 +176,12 @@ 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.toText14(weight: FontWeight.w500)), ], ), ), diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index 91f3d36d..fd003080 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -144,7 +144,7 @@ class _UserInfoSelectionScreenState extends State { Expanded( child: CollapsingListView( title: "Symptoms Checker".needTranslation, - isLeading: true, + isLeading: Navigator.canPop(context), child: SingleChildScrollView( child: Column( children: [ @@ -228,46 +228,48 @@ class _UserInfoSelectionScreenState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), child: SafeArea( top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 24.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: "No, Edit all".needTranslation, - icon: AppAssets.edit_icon, - iconColor: AppColors.primaryRedColor, - onPressed: () { - context.read().setUserInfoPage(0, isSinglePageEdit: false); - context.navigateWithName(AppRoutes.userInfoFlowManager); - }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), - borderColor: Colors.transparent, - textColor: AppColors.primaryRedColor, - fontSize: 16.f, + child: Padding( + padding: EdgeInsets.all(8.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: CustomButton( + text: "No, Edit all".needTranslation, + icon: AppAssets.edit_icon, + iconColor: AppColors.primaryRedColor, + onPressed: () { + context.read().setUserInfoPage(0, isSinglePageEdit: false); + context.navigateWithName(AppRoutes.userInfoFlowManager); + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), ), - ), - SizedBox(width: 12.w), - Expanded( - child: CustomButton( - text: "Yes, It is".needTranslation, - icon: AppAssets.tickIcon, - iconColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, - onPressed: hasEmptyFields - ? () {} // Empty function for disabled state - : () => context.navigateWithName(AppRoutes.organSelectorPage), - backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, - borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, - textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, - fontSize: 16.f, + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Yes, It is".needTranslation, + icon: AppAssets.tickIcon, + iconColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, + onPressed: hasEmptyFields + ? () {} // Empty function for disabled state + : () => context.navigateWithName(AppRoutes.organSelectorPage), + backgroundColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + borderColor: hasEmptyFields ? AppColors.greyLightColor : AppColors.primaryRedColor, + textColor: hasEmptyFields ? AppColors.greyTextColor : AppColors.whiteColor, + fontSize: 16.f, + ), ), - ), - ], - ), - ], - ).paddingSymmetrical(24.w, 0), + ], + ), + ], + ).paddingSymmetrical(24.w, 0), + ), ), ); } diff --git a/lib/presentation/todo_section/todo_page.dart b/lib/presentation/todo_section/todo_page.dart index 8907fc80..0d2d8066 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,15 +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), - SizedBox(height: 24.h), Consumer( builder: (BuildContext context, TodoSectionViewModel todoSectionViewModel, Widget? child) { return todoSectionViewModel.isAncillaryOrdersLoading diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index 8a3e3fa3..78d7e3ef 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; @@ -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 00000000..bd25641a --- /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_screen.dart b/lib/presentation/water_monitor/water_consumption_screen.dart new file mode 100644 index 00000000..5d08bcd6 --- /dev/null +++ b/lib/presentation/water_monitor/water_consumption_screen.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 WaterConsumptionScreen extends StatefulWidget { + const WaterConsumptionScreen({super.key}); + + @override + State createState() => _WaterConsumptionScreenState(); +} + +class _WaterConsumptionScreenState 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: [ + "History".needTranslation.toText16(isBold: true), + Row( + children: [ + 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, + ), + ), + ), + ), + SizedBox(width: 8.w), + InkWell( + onTap: () => _showHistoryDurationBottomsheet(context, viewModel), + child: Container( + padding: EdgeInsets.symmetric(vertical: 6.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), + ], + ), + ), + ) + ], + ), + ], + ), + 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_screen.dart b/lib/presentation/water_monitor/water_monitor_settings_screen.dart new file mode 100644 index 00000000..1b783ca7 --- /dev/null +++ b/lib/presentation/water_monitor/water_monitor_settings_screen.dart @@ -0,0 +1,355 @@ +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:provider/provider.dart'; + +class WaterMonitorSettingsScreen extends StatefulWidget { + const WaterMonitorSettingsScreen({super.key}); + + @override + State createState() => _WaterMonitorSettingsScreenState(); +} + +class _WaterMonitorSettingsScreenState extends State { + // 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, + }) { + 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: 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) { + _showSnackBar(context, viewModel.validationError!); + } else if (success) { + _showSnackBar(context, "Settings saved successfully"); + } + }, + 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, + ), + ], + ), + ), + ), + ); + } + + // Show snackbar for validation errors and success messages + void _showSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 3), + behavior: SnackBarBehavior.floating, + backgroundColor: message.contains('successfully') ? Colors.green : AppColors.errorColor, + ), + ); + } +} 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 00000000..b4b7ba0b --- /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 00000000..4ffa30d9 --- /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 00000000..df558869 --- /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 00000000..6546f776 --- /dev/null +++ b/lib/presentation/water_monitor/widgets/water_action_buttons_widget.dart @@ -0,0 +1,159 @@ +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'); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () 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: isGoalAchieved ? 0.4 : 1.0, + child: InkWell( + onTap: isGoalAchieved + ? 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.waterMonitorSettingsScreen), + 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 00000000..b681e320 --- /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 00000000..08350a1a --- /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 00000000..137f6a38 --- /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 00000000..da5eb069 --- /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 5b18bce2..1a5b6990 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -22,6 +22,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_screen.dart'; +import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; import '../features/qr_parking/qr_parking_view_model.dart'; @@ -52,9 +55,14 @@ class AppRoutes { 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 waterConsumptionScreen = '/waterConsumptionScreen'; + static const String waterMonitorSettingsScreen = '/waterMonitorSettingsScreen'; + // Symptoms Checker static const String organSelectorPage = '/organSelectorPage'; static const String symptomsSelectorScreen = '/symptomsCheckerScreen'; @@ -94,7 +102,12 @@ class AppRoutes { huaweiHealthExample: (context) => HuaweiHealthExample(), covid19Test: (context) => Covid19LandingPage(), // - // healthCalculatorsPage: (context) => HealthCalculatorsPage(), + waterConsumptionScreen: (context) => WaterConsumptionScreen(), + waterMonitorSettingsScreen: (context) => WaterMonitorSettingsScreen(), + healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), + healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), + vitalSign: (context) => VitalSignPage(), + monthlyReports: (context) => ChangeNotifierProvider( create: (_) => MonthlyReportsViewModel( monthlyReportsRepo: getIt(), @@ -102,14 +115,9 @@ class AppRoutes { ), child: const MonthlyReportsPage(), ), - - healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), - healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), qrParking: (context) => ChangeNotifierProvider( create: (_) => getIt(), child: const ParkingPage(), ), - - - }; + }; } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 00000000..fd154c61 --- /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 87d81654..b5366618 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -5,31 +5,31 @@ import 'package:flutter_callkit_incoming/entities/call_event.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; import 'package:get_it/get_it.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/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'; class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + @override _SplashScreenState createState() => _SplashScreenState(); } @@ -48,9 +48,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(); @@ -79,7 +83,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!, @@ -217,7 +222,7 @@ class _SplashScreenState extends State { @override void initState() { - authVm = context.read(); + authVm = getIt(); super.initState(); initializeStuff(); } @@ -226,6 +231,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 2ce5dc1d..eaf48ecc 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -8,6 +8,7 @@ class AppColors { static const bottomSheetBgColor = Color(0xFFF8F8FA); static const lightGreyEFColor = Color(0xffeaeaff); static const greyF7Color = Color(0xffF7F7F7); + static const greyInfoTextColor = Color(0xff777777); static const lightGrayColor = Color(0xff808080); static const greyTextColorLight = Color(0xFFA2A2A2); @@ -32,6 +33,7 @@ class AppColors { static const Color inputLabelTextColor = Color(0xff898A8D); static const Color greyTextColor = Color(0xFF8F9AA3); static const Color lightGrayBGColor = Color(0x142E3039); + static const Color checkBoxBorderColor = Color(0xffD2D2D2); static const Color pharmacyBGColor = Color(0xFF359846); @@ -42,6 +44,7 @@ class AppColors { //Chips static const Color successColor = Color(0xff18C273); + static const Color successLightBgColor = Color(0xffDDF6EA); static const Color errorColor = Color(0xFFED1C2B); static const Color alertColor = Color(0xFFD48D05); static const Color infoColor = Color(0xFF0B85F7); @@ -96,6 +99,15 @@ static const Color lightRedButtonColor = Color(0x1AED1C2B); static const Color eReferralCardColor = Color(0xFFFF8012); static const Color bloodDonationCardColor = Color(0xFFFF5662); static const Color bookAppointment = Color(0xFF415364); + + // Water Monitor + static const Color blueColor = Color(0xFF4EB5FF); + static const Color blueGradientColorOne = Color(0xFFF1F7FD); + static const Color blueGradientColorTwo = Color(0xFFD9EFFF); + + // Shimmer + static const Color shimmerBaseColor = Color(0xFFE0E0E0); + static const Color shimmerHighlightColor = Color(0xFFF5F5F5); static const Color covid29Color = Color(0xff2563EB); static const Color lightGreyTextColor = Color(0xFF959595); static const Color labelColorYellow = Color(0xFFFBCB6E); diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index c1ab6d4c..5409fcf5 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -21,6 +21,7 @@ class CollapsingListView extends StatelessWidget { VoidCallback? history; VoidCallback? instructions; VoidCallback? requests; + VoidCallback? sendEmail; Widget? bottomChild; Widget? trailing; bool isClose; @@ -40,6 +41,7 @@ class CollapsingListView extends StatelessWidget { this.history, this.instructions, this.requests, + this.sendEmail, this.isLeading = true, this.trailing, this.leadingCallback, @@ -78,6 +80,7 @@ class CollapsingListView extends StatelessWidget { history: history, instructions: instructions, requests: requests, + sendEmail: sendEmail, bottomChild: bottomChild, trailing: trailing, ) @@ -86,7 +89,7 @@ class CollapsingListView extends StatelessWidget { ? 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) { @@ -108,6 +111,7 @@ class CollapsingListView extends StatelessWidget { history: history, instructions: instructions, requests: requests, + sendEmail: sendEmail, bottomChild: bottomChild, trailing: trailing, ), @@ -181,6 +185,7 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget VoidCallback? history; VoidCallback? instructions; VoidCallback? requests; + VoidCallback? sendEmail; Widget? bottomChild; Widget? trailing; @@ -195,6 +200,7 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget this.history, this.instructions, this.requests, + this.sendEmail, this.bottomChild, this.trailing, }); @@ -259,6 +265,7 @@ class _ScrollAnimatedTitleState extends State { style: TextStyle( fontSize: _fontSize, fontWeight: FontWeight.bold, + letterSpacing: -1.0, ), ).expanded, ...[ @@ -267,6 +274,7 @@ class _ScrollAnimatedTitleState extends State { 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!, ] diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index a3e117f2..2da1d443 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 676f0bc1..08d281f7 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 8be16b7e..8597b3ed 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/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index b955b329..1f5d8060 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. @@ -56,67 +57,71 @@ 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, + 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.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 +154,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 +167,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 +178,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,32 +197,31 @@ 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(); @@ -236,11 +237,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, @@ -255,4 +256,4 @@ class CustomGraph extends StatelessWidget { return data; } -} \ No newline at end of file +} diff --git a/lib/widgets/shimmer/common_shimmer_widget.dart b/lib/widgets/shimmer/common_shimmer_widget.dart index d6a2906d..3d935cbb 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 00000000..2ab4741c --- /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/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..42b828db --- /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 5590c9e9..461d3aba 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