Platform.TryGetCurrentLocationAsync
All checks were successful
Dotnet build and test / build (pull_request) Successful in 8m30s
All checks were successful
Dotnet build and test / build (pull_request) Successful in 8m30s
This commit is contained in:
parent
55d305f295
commit
914e9486b5
11 changed files with 395 additions and 46 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
using Android.App;
|
using Android.App;
|
||||||
|
using Android;
|
||||||
using Android.Runtime;
|
using Android.Runtime;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Android;
|
using Avalonia.Android;
|
||||||
|
|
@ -9,6 +10,9 @@ using Avalonia.Controls;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Yavsc.Api.Client;
|
using Yavsc.Api.Client;
|
||||||
|
|
||||||
|
[assembly: UsesPermission(Manifest.Permission.AccessFineLocation)]
|
||||||
|
[assembly: UsesPermission(Manifest.Permission.AccessCoarseLocation)]
|
||||||
|
|
||||||
namespace PostIt.Android
|
namespace PostIt.Android
|
||||||
{
|
{
|
||||||
[Application]
|
[Application]
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,17 @@ public class MainActivity : AvaloniaMainActivity
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnRequestPermissionsResult(int requestCode, string[]? permissions, Permission[]? grantResults)
|
||||||
|
{
|
||||||
|
if (PostIt.Android.Services.AndroidCurrentLocationProvider
|
||||||
|
.HandlePermissionResult(requestCode, grantResults))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
}
|
||||||
|
|
||||||
internal static class AndroidOidcCallbackSink
|
internal static class AndroidOidcCallbackSink
|
||||||
{
|
{
|
||||||
private static System.Threading.Tasks.TaskCompletionSource<string>? _pending;
|
private static System.Threading.Tasks.TaskCompletionSource<string>? _pending;
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,12 @@ internal static class PlatformBootstrap
|
||||||
{
|
{
|
||||||
internal static void InitPlatform()
|
internal static void InitPlatform()
|
||||||
{
|
{
|
||||||
|
|
||||||
Platform.CreateBrowser = () =>
|
Platform.CreateBrowser = () =>
|
||||||
{
|
{
|
||||||
var activity = MainActivity.Current;
|
var activity = MainActivity.Current;
|
||||||
return activity is null ? null : new AndroidSystemBrowser(activity);
|
return activity is null ? null : new AndroidSystemBrowser(activity);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Platform.TryGetCurrentLocationAsync = AndroidCurrentLocationProvider.TryGetCurrentLocationAsync;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Android;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content.PM;
|
||||||
|
using Android.Locations;
|
||||||
|
using AndroidX.Core.App;
|
||||||
|
using AndroidX.Core.Content;
|
||||||
|
using PostIt.Services;
|
||||||
|
|
||||||
|
namespace PostIt.Android.Services;
|
||||||
|
|
||||||
|
internal static class AndroidCurrentLocationProvider
|
||||||
|
{
|
||||||
|
public static async Task<CurrentLocationResult> TryGetCurrentLocationAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var activity = MainActivity.Current;
|
||||||
|
if (activity is null)
|
||||||
|
{
|
||||||
|
return CurrentLocationResult.Unavailable("L'activité Android n'est pas encore prête.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissionGranted = await LocationPermissionBroker.EnsureGrantedAsync(activity, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!permissionGranted)
|
||||||
|
{
|
||||||
|
return CurrentLocationResult.PermissionDenied();
|
||||||
|
}
|
||||||
|
|
||||||
|
var locationManager = activity.GetSystemService(global::Android.Content.Context.LocationService) as LocationManager;
|
||||||
|
if (locationManager is null)
|
||||||
|
{
|
||||||
|
return CurrentLocationResult.Unavailable("Le service de localisation Android est indisponible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = locationManager.GetProviders(enabledOnly: true)?
|
||||||
|
.Select(provider => locationManager.GetLastKnownLocation(provider))
|
||||||
|
.Where(candidate => candidate is not null)
|
||||||
|
.OrderByDescending(candidate => candidate!.Time)
|
||||||
|
.ThenBy(candidate => candidate!.Accuracy)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (location is null)
|
||||||
|
{
|
||||||
|
return CurrentLocationResult.Unavailable("Aucune position n'est disponible. Activez la localisation du système puis réessayez.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return CurrentLocationResult.Success(location.Latitude, location.Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HandlePermissionResult(int requestCode, Permission[]? grantResults)
|
||||||
|
=> LocationPermissionBroker.HandleResult(requestCode, grantResults);
|
||||||
|
|
||||||
|
private static class LocationPermissionBroker
|
||||||
|
{
|
||||||
|
private const int RequestCode = 4042;
|
||||||
|
private static readonly string[] RequestedPermissions =
|
||||||
|
{
|
||||||
|
Manifest.Permission.AccessFineLocation,
|
||||||
|
Manifest.Permission.AccessCoarseLocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly object SyncRoot = new();
|
||||||
|
private static TaskCompletionSource<bool>? _pendingRequest;
|
||||||
|
|
||||||
|
public static Task<bool> EnsureGrantedAsync(Activity activity, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (HasLocationPermission(activity))
|
||||||
|
{
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
if (_pendingRequest is null)
|
||||||
|
{
|
||||||
|
_pendingRequest = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
ActivityCompat.RequestPermissions(activity, RequestedPermissions, RequestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancellationToken.CanBeCanceled)
|
||||||
|
{
|
||||||
|
return _pendingRequest.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WaitAsync(_pendingRequest.Task, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HandleResult(int requestCode, Permission[]? grantResults)
|
||||||
|
{
|
||||||
|
if (requestCode != RequestCode)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var granted = grantResults is { Length: > 0 } && grantResults.All(result => result == Permission.Granted);
|
||||||
|
TaskCompletionSource<bool>? pendingRequest;
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
pendingRequest = _pendingRequest;
|
||||||
|
_pendingRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRequest?.TrySetResult(granted);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasLocationPermission(Activity activity)
|
||||||
|
{
|
||||||
|
return ContextCompat.CheckSelfPermission(activity, Manifest.Permission.AccessFineLocation) == Permission.Granted
|
||||||
|
|| ContextCompat.CheckSelfPermission(activity, Manifest.Permission.AccessCoarseLocation) == Permission.Granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> WaitAsync(Task<bool> task, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var registration = cancellationToken.Register(() =>
|
||||||
|
{
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
_pendingRequest?.TrySetCanceled(cancellationToken);
|
||||||
|
_pendingRequest = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using PostIt.Services;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
using Yavsc;
|
using Yavsc;
|
||||||
using Yavsc.Abstract.Workflow;
|
using Yavsc.Abstract.Workflow;
|
||||||
|
|
@ -58,6 +59,61 @@ public class BillingCommandPageViewModelTests
|
||||||
Assert.Contains("n'est pas encore pris en charge", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("n'est pas encore pris en charge", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitAsync_allows_missing_coordinates_and_omits_them_from_payload()
|
||||||
|
{
|
||||||
|
var api = new RecordingApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new BillingCommandPageViewModel(
|
||||||
|
new ActivityBrowseItemDto { Code = "dev", Name = "Développement" },
|
||||||
|
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
|
||||||
|
new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||||
|
client)
|
||||||
|
{
|
||||||
|
EventDateText = "2026-09-02 14:30",
|
||||||
|
Reason = "Point de cadrage",
|
||||||
|
Address = "1 rue du Test",
|
||||||
|
LatitudeText = string.Empty,
|
||||||
|
LongitudeText = string.Empty,
|
||||||
|
Consent = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.SubmitCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody));
|
||||||
|
var location = json.RootElement.GetProperty("Location");
|
||||||
|
Assert.Equal("1 rue du Test", location.GetProperty("Address").GetString());
|
||||||
|
Assert.False(location.TryGetProperty("Latitude", out _));
|
||||||
|
Assert.False(location.TryGetProperty("Longitude", out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UseCurrentLocationAsync_prefills_coordinates_from_platform_provider()
|
||||||
|
{
|
||||||
|
var original = Platform.TryGetCurrentLocationAsync;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Platform.TryGetCurrentLocationAsync = _ => Task.FromResult(CurrentLocationResult.Success(48.8566, 2.3522));
|
||||||
|
|
||||||
|
var api = new RecordingApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new BillingCommandPageViewModel(
|
||||||
|
new ActivityBrowseItemDto { Code = "dev", Name = "Développement" },
|
||||||
|
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
|
||||||
|
new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||||
|
client);
|
||||||
|
|
||||||
|
await vm.UseCurrentLocationCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Equal("48.8566", vm.LatitudeText);
|
||||||
|
Assert.Equal("2.3522", vm.LongitudeText);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Platform.TryGetCurrentLocationAsync = original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation()
|
public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation()
|
||||||
{
|
{
|
||||||
|
|
@ -216,4 +272,4 @@ public class BillingCommandPageViewModelTests
|
||||||
|
|
||||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/PostIt/PostIt/Services/CurrentLocationResult.cs
Normal file
28
src/PostIt/PostIt/Services/CurrentLocationResult.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace PostIt.Services;
|
||||||
|
|
||||||
|
public sealed class CurrentLocationResult
|
||||||
|
{
|
||||||
|
private CurrentLocationResult(bool isSuccess, bool isPermissionDenied, double? latitude, double? longitude, string message)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
IsPermissionDenied = isPermissionDenied;
|
||||||
|
Latitude = latitude;
|
||||||
|
Longitude = longitude;
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
public bool IsPermissionDenied { get; }
|
||||||
|
public double? Latitude { get; }
|
||||||
|
public double? Longitude { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
|
||||||
|
public static CurrentLocationResult Success(double latitude, double longitude, string? message = null)
|
||||||
|
=> new(true, false, latitude, longitude, message ?? "Position récupérée.");
|
||||||
|
|
||||||
|
public static CurrentLocationResult PermissionDenied(string? message = null)
|
||||||
|
=> new(false, true, null, null, message ?? "La géolocalisation n'est pas autorisée.");
|
||||||
|
|
||||||
|
public static CurrentLocationResult Unavailable(string? message = null)
|
||||||
|
=> new(false, false, null, null, message ?? "La géolocalisation n'est pas disponible sur cette plateforme.");
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
using System;
|
||||||
using IdentityModel.OidcClient.Browser;
|
using IdentityModel.OidcClient.Browser;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PostIt.Services;
|
namespace PostIt.Services;
|
||||||
|
|
||||||
|
|
@ -37,4 +40,12 @@ public static class Platform
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static System.Func<IBrowser?>? CreateBrowser { get; set; } =
|
public static System.Func<IBrowser?>? CreateBrowser { get; set; } =
|
||||||
() => new CustomSchemeBrowser(CustomScheme);
|
() => new CustomSchemeBrowser(CustomScheme);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional platform hook used by the shared billing form to request a
|
||||||
|
/// current device position. Platforms that do not expose a native
|
||||||
|
/// location provider can leave the default delegate in place.
|
||||||
|
/// </summary>
|
||||||
|
public static Func<CancellationToken, Task<CurrentLocationResult>> TryGetCurrentLocationAsync { get; set; } =
|
||||||
|
_ => Task.FromResult(CurrentLocationResult.Unavailable());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using PostIt.Services;
|
||||||
using Yavsc;
|
using Yavsc;
|
||||||
using Yavsc.Abstract.Workflow;
|
using Yavsc.Abstract.Workflow;
|
||||||
using Yavsc.Api.Client;
|
using Yavsc.Api.Client;
|
||||||
using Yavsc.Models.Billing;
|
using Yavsc.Models.Billing;
|
||||||
using Yavsc.Models.Haircut;
|
using Yavsc.Models.Haircut;
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
namespace PostIt.ViewModels;
|
||||||
|
|
||||||
|
|
@ -67,6 +67,8 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial QueryStatus CommandStatus { get; set; } = QueryStatus.Inserted;
|
public partial QueryStatus CommandStatus { get; set; } = QueryStatus.Inserted;
|
||||||
|
|
||||||
|
public bool CanUseCurrentLocation => IsSupported && !IsBusy;
|
||||||
|
|
||||||
public string Title => Form.Title;
|
public string Title => Form.Title;
|
||||||
public string PerformerLabel => Performer.UserName;
|
public string PerformerLabel => Performer.UserName;
|
||||||
public string ActivityLabel => Activity.Name;
|
public string ActivityLabel => Activity.Name;
|
||||||
|
|
@ -122,6 +124,12 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
OnPropertyChanged(nameof(SubmitLabel));
|
OnPropertyChanged(nameof(SubmitLabel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnIsBusyChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CanUseCurrentLocation));
|
||||||
|
UseCurrentLocationCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null)
|
public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null)
|
||||||
{
|
{
|
||||||
if (!IsBrush && !IsMultiBrush)
|
if (!IsBrush && !IsMultiBrush)
|
||||||
|
|
@ -198,27 +206,17 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryParseCoordinate(LatitudeText, out var latitude))
|
if (!TryParseCoordinates(out var latitude, out var longitude, out var coordinateError))
|
||||||
{
|
{
|
||||||
StatusMessage = "Latitude invalide.";
|
StatusMessage = coordinateError;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseCoordinate(LongitudeText, out var longitude))
|
|
||||||
{
|
|
||||||
StatusMessage = "Longitude invalide.";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var location = new Location
|
var address = Address.Trim();
|
||||||
{
|
var locationPayload = BuildLocationPayload(address, latitude, longitude);
|
||||||
Address = Address.Trim(),
|
|
||||||
Latitude = latitude,
|
|
||||||
Longitude = longitude,
|
|
||||||
};
|
|
||||||
|
|
||||||
var payload = new BillingQueryDetailsDto
|
var payload = new BillingQueryDetailsDto
|
||||||
{
|
{
|
||||||
|
|
@ -233,9 +231,9 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(),
|
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(),
|
||||||
Location = new BillingLocationDto
|
Location = new BillingLocationDto
|
||||||
{
|
{
|
||||||
Address = location.Address,
|
Address = address,
|
||||||
Latitude = location.Latitude,
|
Latitude = latitude,
|
||||||
Longitude = location.Longitude,
|
Longitude = longitude,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -253,7 +251,7 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
PerformerId = Performer.PerformerId,
|
PerformerId = Performer.PerformerId,
|
||||||
Consent,
|
Consent,
|
||||||
EventDate = eventDate,
|
EventDate = eventDate,
|
||||||
Location = location,
|
Location = locationPayload,
|
||||||
Reason = payload.Reason,
|
Reason = payload.Reason,
|
||||||
Status = payload.Status,
|
Status = payload.Status,
|
||||||
}).ConfigureAwait(true);
|
}).ConfigureAwait(true);
|
||||||
|
|
@ -281,7 +279,7 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
PerformerId = Performer.PerformerId,
|
PerformerId = Performer.PerformerId,
|
||||||
Consent,
|
Consent,
|
||||||
EventDate = (DateTime?)eventDate,
|
EventDate = (DateTime?)eventDate,
|
||||||
Location = location,
|
Location = locationPayload,
|
||||||
PrestationId = SelectedPrestation.Id,
|
PrestationId = SelectedPrestation.Id,
|
||||||
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(),
|
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(),
|
||||||
Status = payload.Status,
|
Status = payload.Status,
|
||||||
|
|
@ -311,7 +309,7 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
PerformerId = Performer.PerformerId,
|
PerformerId = Performer.PerformerId,
|
||||||
Consent,
|
Consent,
|
||||||
EventDate = eventDate,
|
EventDate = eventDate,
|
||||||
Location = location,
|
Location = locationPayload,
|
||||||
Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(),
|
Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(),
|
||||||
Status = payload.Status,
|
Status = payload.Status,
|
||||||
}).ConfigureAwait(true);
|
}).ConfigureAwait(true);
|
||||||
|
|
@ -336,6 +334,44 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanUseCurrentLocation))]
|
||||||
|
private async Task UseCurrentLocationAsync()
|
||||||
|
{
|
||||||
|
if (!CanUseCurrentLocation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await Platform.TryGetCurrentLocationAsync(default).ConfigureAwait(true);
|
||||||
|
if (!result.IsSuccess || !result.Latitude.HasValue || !result.Longitude.HasValue)
|
||||||
|
{
|
||||||
|
StatusMessage = result.Message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LatitudeText = result.Latitude.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
LongitudeText = result.Longitude.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
StatusMessage = string.IsNullOrWhiteSpace(Address)
|
||||||
|
? "Position récupérée. Complétez l'adresse puis envoyez la commande."
|
||||||
|
: result.Message;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
StatusMessage = "La récupération de la position a été annulée.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Impossible de récupérer la position: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
|
private void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
|
||||||
{
|
{
|
||||||
ExistingQueryId = existingQuery.Id;
|
ExistingQueryId = existingQuery.Id;
|
||||||
|
|
@ -354,8 +390,8 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
if (existingQuery.Location is not null)
|
if (existingQuery.Location is not null)
|
||||||
{
|
{
|
||||||
Address = existingQuery.Location.Address ?? string.Empty;
|
Address = existingQuery.Location.Address ?? string.Empty;
|
||||||
LatitudeText = existingQuery.Location.Latitude.ToString(CultureInfo.InvariantCulture);
|
LatitudeText = existingQuery.Location.Latitude?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
||||||
LongitudeText = existingQuery.Location.Longitude.ToString(CultureInfo.InvariantCulture);
|
LongitudeText = existingQuery.Location.Longitude?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsBrush && existingQuery.PrestationId is not null)
|
if (IsBrush && existingQuery.PrestationId is not null)
|
||||||
|
|
@ -396,4 +432,59 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
||||||
return double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out value)
|
return double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out value)
|
||||||
|| double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out value);
|
|| double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object BuildLocationPayload(string address, double? latitude, double? longitude)
|
||||||
|
{
|
||||||
|
if (latitude.HasValue && longitude.HasValue)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
Latitude = latitude.Value,
|
||||||
|
Longitude = longitude.Value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryParseCoordinates(out double? latitude, out double? longitude, out string error)
|
||||||
|
{
|
||||||
|
latitude = null;
|
||||||
|
longitude = null;
|
||||||
|
error = string.Empty;
|
||||||
|
|
||||||
|
var latitudeMissing = string.IsNullOrWhiteSpace(LatitudeText);
|
||||||
|
var longitudeMissing = string.IsNullOrWhiteSpace(LongitudeText);
|
||||||
|
|
||||||
|
if (latitudeMissing && longitudeMissing)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latitudeMissing != longitudeMissing)
|
||||||
|
{
|
||||||
|
error = "Latitude et longitude doivent être renseignées ensemble, ou laissées vides toutes les deux.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryParseCoordinate(LatitudeText, out var parsedLatitude))
|
||||||
|
{
|
||||||
|
error = "Latitude invalide.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryParseCoordinate(LongitudeText, out var parsedLongitude))
|
||||||
|
{
|
||||||
|
error = "Longitude invalide.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
latitude = parsedLatitude;
|
||||||
|
longitude = parsedLongitude;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
x:DataType="vm:BillingCommandPageViewModel"
|
x:DataType="vm:BillingCommandPageViewModel"
|
||||||
Header="Commande billing">
|
Header="Commande billing">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" Margin="12">
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" Margin="12">
|
||||||
<TextBlock Grid.Row="0" Grid.ColumnSpan="2"
|
<TextBlock Grid.Row="0" Grid.ColumnSpan="2"
|
||||||
Text="{Binding Title}"
|
Text="{Binding Title}"
|
||||||
FontSize="18"
|
FontSize="18"
|
||||||
|
|
@ -40,18 +40,25 @@
|
||||||
<TextBlock Grid.Row="6" Text="Adresse" VerticalAlignment="Center" Margin="0,0,12,8" />
|
<TextBlock Grid.Row="6" Text="Adresse" VerticalAlignment="Center" Margin="0,0,12,8" />
|
||||||
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding Address, Mode=TwoWay}" Margin="0,0,0,8" />
|
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding Address, Mode=TwoWay}" Margin="0,0,0,8" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="7" Text="Latitude" VerticalAlignment="Center" Margin="0,0,12,8" />
|
<Button Grid.Row="7"
|
||||||
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding LatitudeText, Mode=TwoWay}" Margin="0,0,0,8" />
|
Grid.ColumnSpan="2"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Margin="0,0,0,8"
|
||||||
|
Content="Utiliser ma position"
|
||||||
|
Command="{Binding UseCurrentLocationCommand}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="8" Text="Longitude" VerticalAlignment="Center" Margin="0,0,12,8" />
|
<TextBlock Grid.Row="8" Text="Latitude" VerticalAlignment="Center" Margin="0,0,12,8" />
|
||||||
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding LongitudeText, Mode=TwoWay}" Margin="0,0,0,8" />
|
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding LatitudeText, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="9"
|
<TextBlock Grid.Row="9" Text="Longitude" VerticalAlignment="Center" Margin="0,0,12,8" />
|
||||||
|
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding LongitudeText, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="10"
|
||||||
Text="Prestation"
|
Text="Prestation"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0,0,12,8"
|
Margin="0,0,12,8"
|
||||||
IsVisible="{Binding ShowsSinglePrestation}" />
|
IsVisible="{Binding ShowsSinglePrestation}" />
|
||||||
<ComboBox Grid.Row="9"
|
<ComboBox Grid.Row="10"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
ItemsSource="{Binding AvailablePrestations}"
|
ItemsSource="{Binding AvailablePrestations}"
|
||||||
SelectedItem="{Binding SelectedPrestation, Mode=TwoWay}"
|
SelectedItem="{Binding SelectedPrestation, Mode=TwoWay}"
|
||||||
|
|
@ -67,12 +74,12 @@
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<TextBlock Grid.Row="10"
|
<TextBlock Grid.Row="11"
|
||||||
Text="Prestations"
|
Text="Prestations"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Margin="0,0,12,8"
|
Margin="0,0,12,8"
|
||||||
IsVisible="{Binding ShowsMultiplePrestations}" />
|
IsVisible="{Binding ShowsMultiplePrestations}" />
|
||||||
<ListBox Grid.Row="10"
|
<ListBox Grid.Row="11"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
ItemsSource="{Binding MultiPrestations}"
|
ItemsSource="{Binding MultiPrestations}"
|
||||||
IsVisible="{Binding ShowsMultiplePrestations}"
|
IsVisible="{Binding ShowsMultiplePrestations}"
|
||||||
|
|
@ -90,23 +97,23 @@
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<TextBlock Grid.Row="11"
|
<TextBlock Grid.Row="12"
|
||||||
Text="Informations"
|
Text="Informations"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0,0,12,8"
|
Margin="0,0,12,8"
|
||||||
IsVisible="{Binding ShowsAdditionalInfo}" />
|
IsVisible="{Binding ShowsAdditionalInfo}" />
|
||||||
<TextBox Grid.Row="11"
|
<TextBox Grid.Row="12"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Text="{Binding AdditionalInfo, Mode=TwoWay}"
|
Text="{Binding AdditionalInfo, Mode=TwoWay}"
|
||||||
Margin="0,0,0,8"
|
Margin="0,0,0,8"
|
||||||
IsVisible="{Binding ShowsAdditionalInfo}" />
|
IsVisible="{Binding ShowsAdditionalInfo}" />
|
||||||
|
|
||||||
<CheckBox Grid.Row="12" Grid.ColumnSpan="2"
|
<CheckBox Grid.Row="13" Grid.ColumnSpan="2"
|
||||||
Content="Je consens à la création de cette commande"
|
Content="Je consens à la création de cette commande"
|
||||||
IsChecked="{Binding Consent, Mode=TwoWay}"
|
IsChecked="{Binding Consent, Mode=TwoWay}"
|
||||||
Margin="0,4,0,12" />
|
Margin="0,4,0,12" />
|
||||||
|
|
||||||
<Grid Grid.Row="13" Grid.ColumnSpan="2" ColumnDefinitions="Auto,12,*,Auto">
|
<Grid Grid.Row="14" Grid.ColumnSpan="2" ColumnDefinitions="Auto,12,*,Auto">
|
||||||
<Button Grid.Column="0"
|
<Button Grid.Column="0"
|
||||||
Content="{Binding SubmitLabel}"
|
Content="{Binding SubmitLabel}"
|
||||||
Command="{Binding SubmitCommand}"
|
Command="{Binding SubmitCommand}"
|
||||||
|
|
|
||||||
|
|
@ -286,12 +286,22 @@ public sealed class BillingApiClient
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new
|
var payload = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
Address = location.Address,
|
["Address"] = location.Address,
|
||||||
Latitude = location.Latitude,
|
|
||||||
Longitude = location.Longitude,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (location.Latitude.HasValue)
|
||||||
|
{
|
||||||
|
payload["Latitude"] = location.Latitude.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.Longitude.HasValue)
|
||||||
|
{
|
||||||
|
payload["Longitude"] = location.Longitude.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class BillingLocationResponse
|
private sealed class BillingLocationResponse
|
||||||
|
|
@ -351,4 +361,4 @@ public sealed class BillingApiClient
|
||||||
{
|
{
|
||||||
public long PrestationId { get; set; }
|
public long PrestationId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,6 @@ public sealed class BillingQueryDetailsDto
|
||||||
public sealed class BillingLocationDto
|
public sealed class BillingLocationDto
|
||||||
{
|
{
|
||||||
public string Address { get; set; } = string.Empty;
|
public string Address { get; set; } = string.Empty;
|
||||||
public double Latitude { get; set; }
|
public double? Latitude { get; set; }
|
||||||
public double Longitude { get; set; }
|
public double? Longitude { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue