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/svg/confirm.svg b/assets/images/svg/confirm.svg index 5ed3693b..62cfa014 100755 --- a/assets/images/svg/confirm.svg +++ b/assets/images/svg/confirm.svg @@ -1,3 +1,3 @@ -add_ + 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/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_consts.dart b/lib/core/api_consts.dart index 0febdbee..1846d1e0 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -675,7 +675,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 6ba29808..a581e786 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -246,6 +246,8 @@ 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'; diff --git a/lib/core/cache_consts.dart b/lib/core/cache_consts.dart index 8deb8bde..c1e06aa3 100644 --- a/lib/core/cache_consts.dart +++ b/lib/core/cache_consts.dart @@ -75,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/dependencies.dart b/lib/core/dependencies.dart index 489af592..ebf0a844 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -30,10 +30,14 @@ import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_repo.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; @@ -138,6 +142,8 @@ class AppDependencies { 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())); // ViewModels // Global/shared VMs → LazySingleton @@ -271,5 +277,9 @@ class AppDependencies { getIt.registerLazySingleton(() => HealthProvider()); getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt())); + + getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + + getIt.registerLazySingleton(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); } } 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/utils.dart b/lib/core/utils/utils.dart index 0302818b..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) @@ -868,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 { diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 75c57a79..309dde19 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -192,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( 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/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_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/main.dart b/lib/main.dart index 9600d5e2..eb57c374 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,9 +22,11 @@ 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'; @@ -169,6 +171,12 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), 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/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 64dc803f..39e4e03d 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -358,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, ); } 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/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 0ee7a335..72658bfd 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -182,8 +182,9 @@ class _ReviewAppointmentPageState extends State { children: [ bookAppointmentsViewModel.selectedDoctor.projectName!.toText16(isBold: true), SizedBox(height: 8.h), - Wrap( - direction: Axis.horizontal, + bookAppointmentsViewModel.appointmentNearestGateResponseModel != null + ? Wrap( + direction: Axis.horizontal, spacing: 8.w, runSpacing: 8.h, children: [ @@ -197,7 +198,8 @@ class _ReviewAppointmentPageState extends State { "Nearest Gate: ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), ], - ), + ) + : SizedBox.shrink(), ], ), ), 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/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 1380ad9c..f2cab90b 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -10,6 +10,7 @@ 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'; @@ -17,6 +18,7 @@ import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_mo 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 +50,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 +72,7 @@ class ServicesPage extends StatelessWidget { AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, - route: AppRoutes.eReferralPage, + route: AppRoutes.bookAppointmentPage, ), HmgServicesComponentModel( 5, @@ -69,6 +83,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, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 1496ded9..b25ae823 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'; @@ -67,13 +73,13 @@ class _LandingPageState extends State { late AppState appState; late MyAppointmentsViewModel myAppointmentsViewModel; - late PrescriptionsViewModel prescriptionsViewModel; final CacheService cacheService = GetIt.instance(); late AppointmentRatingViewModel appointmentRatingViewModel; late InsuranceViewModel insuranceViewModel; late ImmediateLiveCareViewModel immediateLiveCareViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late EmergencyServicesViewModel emergencyServicesViewModel; + late TodoSectionViewModel todoSectionViewModel; final SwiperController _controller = SwiperController(); @@ -82,10 +88,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) { @@ -97,13 +99,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) { @@ -111,17 +111,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); - - })); - - } - }); + }), + ); + } + }, + ); } } }); @@ -132,10 +131,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, @@ -193,7 +192,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(), @@ -214,6 +213,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: [ @@ -234,8 +277,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( @@ -452,20 +495,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/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 5683c94a..6dde9580 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'; @@ -23,8 +25,10 @@ import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/patient_sickleave_response_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_report/monthly_report_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/allergies/allergies_list_page.dart'; @@ -48,10 +52,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'; @@ -83,7 +91,12 @@ class _MedicalFilePageState extends State { late MedicalFileViewModel medicalFileViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late LabViewModel labViewModel; + late MyInvoicesViewModel myInvoicesViewModel; late HmgServicesViewModel hmgServicesViewModel; + late PrescriptionsViewModel prescriptionsViewModel; + late MonthlyReportViewModel monthlyReportViewModel; + + final CacheService cacheService = GetIt.instance(); int currentIndex = 0; @@ -96,7 +109,8 @@ class _MedicalFilePageState extends State { appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { - labViewModel.initLabProvider(); + myAppointmentsViewModel.getPatientMyDoctors(); + prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); @@ -131,10 +145,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( @@ -1133,7 +1151,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, @@ -1141,7 +1165,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), @@ -1196,7 +1226,14 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.monthly_reports_icon, isLargeText: true, iconSize: 36.h, - ), + ).onPress(() { + monthlyReportViewModel.setHealthSummaryEnabled(cacheService.getBool(key: CacheConst.isMonthlyReportEnabled) ?? false); + Navigator.of(context).push( + CustomPageRoute( + page: MonthlyReport(), + ), + ); + }), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, 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 a6c69654..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, 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/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/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/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"