Add backend API client and remove old client RFID service
Adds a reusable backend API client for scanner and settings operations, replacing the older ClientRfidService with a broader HTTP client abstraction.feature/centralized-offline-canteen
parent
8bc0e215f7
commit
891ddc9175
|
|
@ -0,0 +1,336 @@
|
|||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using UtopiaCanteen.Shared;
|
||||
using UtopiaCanteenSystem.Api;
|
||||
using UtopiaCanteenSystem.Models;
|
||||
|
||||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Frontend HTTP client: all scan/history/stats and settings actions go through the central backend API.
|
||||
/// </summary>
|
||||
public class CanteenBackendApiClient : ICanteenBackendApiClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfigService _config;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public CanteenBackendApiClient(HttpClient http, IConfigService config)
|
||||
{
|
||||
_http = http;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
private string BaseUrl => (_config.GetBackendBaseUrl() ?? string.Empty).TrimEnd('/');
|
||||
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/health";
|
||||
var resp = await _http.GetFromJsonAsync<HealthResponse>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return resp != null && string.Equals(resp.Status, "ok", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "CanteenBackendApiClient.HealthCheckAsync");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ManualSyncResponse> SyncCacheNowAsync(CancellationToken cancellationToken = default) =>
|
||||
await PostJobAsync("/api/cache/sync-now", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task<ManualSyncResponse> SyncOrdersNowAsync(CancellationToken cancellationToken = default) =>
|
||||
await PostJobAsync("/api/orders/sync-now", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task<CacheStatusResponse?> GetCacheStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/cache/status";
|
||||
return await _http.GetFromJsonAsync<CacheStatusResponse>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "CanteenBackendApiClient.GetCacheStatusAsync");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public (bool Success, string Message) ProcessScan(string cardId)
|
||||
{
|
||||
var r = ProcessScanDetailed(cardId);
|
||||
return (r.Success, r.Message);
|
||||
}
|
||||
|
||||
public ScanResult ProcessScanDetailed(string cardId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return new ScanResult(false, "Backend URL is not configured.", 0);
|
||||
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/scan";
|
||||
var body = new ScanRequest
|
||||
{
|
||||
CardId = cardId?.Trim() ?? string.Empty,
|
||||
DeviceId = _config.GetDeviceId(),
|
||||
SiteId = _config.GetSiteId(),
|
||||
IpAddress = null
|
||||
};
|
||||
|
||||
using var response = _http.PostAsJsonAsync(url, body, JsonOptions).GetAwaiter().GetResult();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var err = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
return new ScanResult(false,
|
||||
$"Backend error ({(int)response.StatusCode}): {Truncate(err, 200)}", 0);
|
||||
}
|
||||
|
||||
var dto = response.Content.ReadFromJsonAsync<ScanResponse>(JsonOptions).GetAwaiter().GetResult();
|
||||
if (dto == null)
|
||||
return new ScanResult(false, "Invalid response from backend.", 0);
|
||||
|
||||
return new ScanResult(
|
||||
dto.Success,
|
||||
dto.Message ?? string.Empty,
|
||||
dto.CooldownSecondsRemaining,
|
||||
dto.Employee == null ? null : ApiDtoMapper.ToModel(dto.Employee),
|
||||
(MealSession)dto.MealSession,
|
||||
dto.EmployeeSiteId,
|
||||
dto.CurrentSiteId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "CanteenBackendApiClient.ProcessScanDetailed");
|
||||
return new ScanResult(false, "Cannot reach backend: " + ex.Message, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public ScanRecord? GetLastScan() =>
|
||||
GetAsync<RecentScanDto?, ScanRecord?>("/api/rfid/scans/last", d => d == null ? null : ApiDtoMapper.ToEntity(d));
|
||||
|
||||
public IReadOnlyList<ScanRecord> GetLastScans(int count)
|
||||
{
|
||||
if (count <= 0) return Array.Empty<ScanRecord>();
|
||||
return GetAsync<List<RecentScanDto>, IReadOnlyList<ScanRecord>>(
|
||||
$"/api/rfid/scans/recent?count={count}",
|
||||
list => list?.Select(ApiDtoMapper.ToEntity).ToList() ?? new List<ScanRecord>());
|
||||
}
|
||||
|
||||
public IReadOnlyList<ScanRecord> GetScansForToday() =>
|
||||
GetAsync<List<RecentScanDto>, IReadOnlyList<ScanRecord>>(
|
||||
"/api/rfid/scans/today",
|
||||
list => list?.Select(ApiDtoMapper.ToEntity).ToList() ?? new List<ScanRecord>());
|
||||
|
||||
public int GetTodayScanCount() => GetTodayScanCountAsync().GetAwaiter().GetResult();
|
||||
|
||||
public async Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default) =>
|
||||
await GetCountAsync("/api/rfid/stats/today", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default) =>
|
||||
await GetCountAsync("/api/rfid/stats/total", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cardId)) return 0;
|
||||
return await GetCountAsync($"/api/rfid/stats/total-for-card?cardId={Uri.EscapeDataString(cardId.Trim())}", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> GetTodayScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cardId)) return 0;
|
||||
return await GetCountAsync($"/api/rfid/stats/today-for-card?cardId={Uri.EscapeDataString(cardId.Trim())}", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void UpdateLastScanMealInfo(string mealLabel, string mealItems, double totalPrice)
|
||||
{
|
||||
// Backend owns SQLite; no local update on frontend.
|
||||
}
|
||||
|
||||
private async Task<ManualSyncResponse> PostJobAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
{
|
||||
return new ManualSyncResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Backend URL is not configured."
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}{path}";
|
||||
using var response = await _http.PostAsync(url, null, cancellationToken).ConfigureAwait(false);
|
||||
var dto = await response.Content.ReadFromJsonAsync<ManualSyncResponse>(JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (dto != null)
|
||||
return dto;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var err = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
return new ManualSyncResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Backend error ({(int)response.StatusCode}): {Truncate(err, 200)}"
|
||||
};
|
||||
}
|
||||
|
||||
return new ManualSyncResponse { Success = false, Message = "Invalid response from backend." };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, $"CanteenBackendApiClient.PostJobAsync{path}");
|
||||
return new ManualSyncResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Cannot reach backend: " + ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> GetCountAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl)) return 0;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}{path}";
|
||||
var wrap = await _http.GetFromJsonAsync<StatsDto>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return wrap?.Count ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, $"CanteenBackendApiClient.GetCountAsync{path}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private TResult GetAsync<TResponse, TResult>(string path, Func<TResponse?, TResult> map)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return map(default);
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}{path}";
|
||||
var data = _http.GetFromJsonAsync<TResponse>(url, JsonOptions).GetAwaiter().GetResult();
|
||||
return map(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, $"CanteenBackendApiClient.GetAsync{path}");
|
||||
return map(default);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<HealthResponse?> GetHealthAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
return await _http.GetFromJsonAsync<HealthResponse>($"{BaseUrl}/api/health", JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "CanteenBackendApiClient.GetHealthAsync");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<MealScheduleDto>> GetMealSchedulesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return Array.Empty<MealScheduleDto>();
|
||||
try
|
||||
{
|
||||
var list = await _http.GetFromJsonAsync<List<MealScheduleDto>>($"{BaseUrl}/api/meal-schedules", JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return list ?? new List<MealScheduleDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "CanteenBackendApiClient.GetMealSchedulesAsync");
|
||||
return Array.Empty<MealScheduleDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<long> CreateMealScheduleAsync(MealScheduleDto schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureBaseUrl();
|
||||
var url = $"{BaseUrl}/api/meal-schedules";
|
||||
using var response = await _http.PostAsJsonAsync(url, schedule, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
await EnsureSuccessAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
var created = await response.Content.ReadFromJsonAsync<MealScheduleDto>(JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return created?.Id ?? 0;
|
||||
}
|
||||
|
||||
public async Task UpdateMealScheduleAsync(MealScheduleDto schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureBaseUrl();
|
||||
var url = $"{BaseUrl}/api/meal-schedules/{schedule.Id}";
|
||||
using var response = await _http.PutAsJsonAsync(url, schedule, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
await EnsureSuccessAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeleteMealScheduleAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureBaseUrl();
|
||||
var url = $"{BaseUrl}/api/meal-schedules/{id}";
|
||||
using var response = await _http.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
await EnsureSuccessAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<string?> GetEmployeeLocationSiteAsync(string employeeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl) || string.IsNullOrWhiteSpace(employeeId))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/employees/location-site?employeeId={Uri.EscapeDataString(employeeId.Trim())}";
|
||||
var resp = await _http.GetFromJsonAsync<EmployeeLocationSiteResponse>(url, JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return string.IsNullOrWhiteSpace(resp?.LocationSiteId) ? null : resp.LocationSiteId.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "CanteenBackendApiClient.GetEmployeeLocationSiteAsync");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureBaseUrl()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
throw new InvalidOperationException("Backend URL is not configured.");
|
||||
}
|
||||
|
||||
private static async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
var err = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new HttpRequestException($"Backend error ({(int)response.StatusCode}): {Truncate(err, 300)}");
|
||||
}
|
||||
|
||||
private static string Truncate(string s, int max)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s) || s.Length <= max) return s;
|
||||
return s.Substring(0, max) + "…";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using UtopiaCanteenSystem.Api;
|
||||
using UtopiaCanteenSystem.Models;
|
||||
|
||||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Scanner PC: forwards scans and dashboard queries to the central server HTTP API (no local transaction DB).
|
||||
/// </summary>
|
||||
public class ClientRfidService : IRfidService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfigService _config;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public ClientRfidService(HttpClient http, IConfigService config)
|
||||
{
|
||||
_http = http;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
private string BaseUrl => (_config.GetCentralServerBaseUrl() ?? string.Empty).TrimEnd('/');
|
||||
|
||||
public (bool Success, string Message) ProcessScan(string cardId)
|
||||
{
|
||||
var r = ProcessScanDetailed(cardId);
|
||||
return (r.Success, r.Message);
|
||||
}
|
||||
|
||||
public ScanResult ProcessScanDetailed(string cardId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return new ScanResult(false, "Central server URL is not configured.", 0);
|
||||
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/scan";
|
||||
var body = new RfidScanRequestDto
|
||||
{
|
||||
CardId = cardId?.Trim() ?? string.Empty,
|
||||
DeviceId = _config.GetDeviceId(),
|
||||
SiteId = _config.GetSiteId(),
|
||||
IpAddress = null
|
||||
};
|
||||
|
||||
using var response = _http.PostAsJsonAsync(url, body, JsonOptions).GetAwaiter().GetResult();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var err = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
return new ScanResult(false,
|
||||
$"Central server error ({(int)response.StatusCode}): {Truncate(err, 200)}", 0);
|
||||
}
|
||||
|
||||
var dto = response.Content.ReadFromJsonAsync<RfidScanResponseDto>(JsonOptions).GetAwaiter().GetResult();
|
||||
if (dto == null)
|
||||
return new ScanResult(false, "Invalid response from central server.", 0);
|
||||
|
||||
return new ScanResult(
|
||||
dto.Success,
|
||||
dto.Message ?? string.Empty,
|
||||
dto.CooldownSecondsRemaining,
|
||||
dto.Employee?.ToModel(),
|
||||
(MealSession)dto.MealSession,
|
||||
dto.EmployeeSiteId,
|
||||
dto.CurrentSiteId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.ProcessScanDetailed");
|
||||
return new ScanResult(false, "Cannot reach central server: " + ex.Message, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public ScanRecord? GetLastScan()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/scans/last";
|
||||
var dto = _http.GetFromJsonAsync<ScanRecordDto?>(url, JsonOptions).GetAwaiter().GetResult();
|
||||
return dto?.ToEntity();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetLastScan");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ScanRecord> GetLastScans(int count)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl) || count <= 0)
|
||||
return Array.Empty<ScanRecord>();
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/scans/recent?count={count}";
|
||||
var list = _http.GetFromJsonAsync<List<ScanRecordDto>>(url, JsonOptions).GetAwaiter().GetResult();
|
||||
return list?.Select(d => d.ToEntity()).ToList() ?? new List<ScanRecord>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetLastScans");
|
||||
return Array.Empty<ScanRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ScanRecord> GetScansForToday()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return Array.Empty<ScanRecord>();
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/scans/today";
|
||||
var list = _http.GetFromJsonAsync<List<ScanRecordDto>>(url, JsonOptions).GetAwaiter().GetResult();
|
||||
return list?.Select(d => d.ToEntity()).ToList() ?? new List<ScanRecord>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetScansForToday");
|
||||
return Array.Empty<ScanRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
public int GetTodayScanCount() => GetTodayScanCountAsync().GetAwaiter().GetResult();
|
||||
|
||||
public async Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/stats/today";
|
||||
var wrap = await _http.GetFromJsonAsync<CountDto>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return wrap?.Count ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetTodayScanCountAsync");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalScanCountAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/stats/total";
|
||||
var wrap = await _http.GetFromJsonAsync<CountDto>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return wrap?.Count ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetTotalScanCountAsync");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl) || string.IsNullOrWhiteSpace(cardId))
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/stats/total-for-card?cardId={Uri.EscapeDataString(cardId.Trim())}";
|
||||
var wrap = await _http.GetFromJsonAsync<CountDto>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return wrap?.Count ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetTotalScanCountForCardAsync");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetTodayScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl) || string.IsNullOrWhiteSpace(cardId))
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/api/rfid/stats/today-for-card?cardId={Uri.EscapeDataString(cardId.Trim())}";
|
||||
var wrap = await _http.GetFromJsonAsync<CountDto>(url, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return wrap?.Count ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(ex, "ClientRfidService.GetTodayScanCountForCardAsync");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLastScanMealInfo(string mealLabel, string mealItems, double totalPrice)
|
||||
{
|
||||
// Server owns SQLite; client has nothing to update locally.
|
||||
}
|
||||
|
||||
private static string Truncate(string s, int max)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s) || s.Length <= max)
|
||||
return s;
|
||||
return s.Substring(0, max) + "…";
|
||||
}
|
||||
|
||||
private sealed class CountDto
|
||||
{
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using UtopiaCanteen.Shared;
|
||||
|
||||
namespace UtopiaCanteenSystem.Services;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for the central canteen backend API (scanner UI and settings).
|
||||
/// </summary>
|
||||
public interface ICanteenBackendApiClient : IRfidService
|
||||
{
|
||||
Task<bool> HealthCheckAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ManualSyncResponse> SyncCacheNowAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ManualSyncResponse> SyncOrdersNowAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CacheStatusResponse?> GetCacheStatusAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<HealthResponse?> GetHealthAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<MealScheduleDto>> GetMealSchedulesAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<long> CreateMealScheduleAsync(MealScheduleDto schedule, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpdateMealScheduleAsync(MealScheduleDto schedule, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeleteMealScheduleAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<string?> GetEmployeeLocationSiteAsync(string employeeId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
Loading…
Reference in New Issue