Initial commit: admin login + scanner flow + sqlite persistence + sync service
commit
113b22f344
|
|
@ -0,0 +1,28 @@
|
||||||
|
<Application x:Class="UtopiaCanteenSystem.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:UtopiaCanteenSystem.ViewModels"
|
||||||
|
xmlns:views="clr-namespace:UtopiaCanteenSystem.Views"
|
||||||
|
>
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
|
||||||
|
<!-- DataTemplates: map ViewModel type to View for MainWindow ContentControl -->
|
||||||
|
<DataTemplate DataType="{x:Type vm:AdminLoginViewModel}">
|
||||||
|
<views:AdminLoginView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:AdminSettingsAuthViewModel}">
|
||||||
|
<views:AdminSettingsAuthView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:ScannerViewModel}">
|
||||||
|
<views:ScannerView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:MainDashboardViewModel}">
|
||||||
|
<views:MainDashboardView />
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
|
||||||
|
<views:SettingsView />
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System.Windows;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UtopiaCanteenSystem.Data;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
using UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application entry point. Initializes database, builds service graph, starts hourly sync timer.
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private System.Timers.Timer? _syncTimer;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// Build services (simple composition; no DI container)
|
||||||
|
var dbFactory = new DbContextFactory();
|
||||||
|
|
||||||
|
// Auto-create SQLite database on first run
|
||||||
|
using (var db = dbFactory.CreateDbContext())
|
||||||
|
{
|
||||||
|
db.EnsureDatabaseCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
var configService = new ConfigService();
|
||||||
|
var rfidService = new RfidService(dbFactory, configService);
|
||||||
|
var syncService = new SyncService(dbFactory, configService);
|
||||||
|
var session = new AppSession();
|
||||||
|
var authenticationUrl = "https://portal.utopiaindustries.pk/uind/rest/auth/user/";
|
||||||
|
var authService = new AuthService(authenticationUrl);
|
||||||
|
|
||||||
|
// NavigationService: declare first so lambdas can capture it, then assign (avoids "used before declared")
|
||||||
|
NavigationService navigationService = null!;
|
||||||
|
navigationService = new NavigationService(
|
||||||
|
session,
|
||||||
|
() => new AdminLoginViewModel(authService, session, navigationService),
|
||||||
|
() => new ScannerViewModel(rfidService, navigationService, session),
|
||||||
|
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
|
||||||
|
() => new AdminSettingsAuthViewModel(authService, session, navigationService),
|
||||||
|
() => new SettingsViewModel(configService, navigationService));
|
||||||
|
|
||||||
|
var mainViewModel = new MainViewModel(navigationService);
|
||||||
|
|
||||||
|
var mainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = mainViewModel
|
||||||
|
};
|
||||||
|
mainWindow.Show();
|
||||||
|
|
||||||
|
// Hourly background sync: every 1 hour, POST unsynced ScanRecords to API
|
||||||
|
_syncTimer = new System.Timers.Timer(TimeSpan.FromHours(1).TotalMilliseconds);
|
||||||
|
_syncTimer.Elapsed += async (_, _) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await syncService.SyncNowAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore; will retry next hour
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_syncTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
_syncTimer?.Stop();
|
||||||
|
_syncTimer?.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Converters
|
||||||
|
{
|
||||||
|
public class StringLengthToVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
// Binding is typically Text.Length (int): show watermark when empty (0), hide when has text
|
||||||
|
if (value is int length)
|
||||||
|
return length == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (value is string s)
|
||||||
|
return string.IsNullOrEmpty(s) ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
return Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return Binding.DoNothing; // No reverse conversion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UtopiaCanteenSystem.Models;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite DbContext for Labour and ScanRecord tables.
|
||||||
|
/// Database file is created in application directory on first run.
|
||||||
|
/// </summary>
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
private static readonly string DbPath = Path.Combine(
|
||||||
|
AppDomain.CurrentDomain.BaseDirectory,
|
||||||
|
"utopia_canteen.db");
|
||||||
|
|
||||||
|
public DbSet<Labour> Labour { get; set; }
|
||||||
|
public DbSet<ScanRecord> ScanRecords { get; set; }
|
||||||
|
|
||||||
|
public AppDbContext() { }
|
||||||
|
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
if (!optionsBuilder.IsConfigured)
|
||||||
|
optionsBuilder.UseSqlite($"Data Source={DbPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
// Labour: CardId should be indexed for lookups.
|
||||||
|
modelBuilder.Entity<Labour>(e =>
|
||||||
|
{
|
||||||
|
e.HasKey(x => x.Id);
|
||||||
|
e.HasIndex(x => x.CardId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ScanRecord: Index for unsynced queries and by ScanTime.
|
||||||
|
modelBuilder.Entity<ScanRecord>(e =>
|
||||||
|
{
|
||||||
|
e.HasKey(x => x.Id);
|
||||||
|
e.HasIndex(x => x.IsSynced);
|
||||||
|
e.HasIndex(x => x.ScanTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures database exists and is migrated. Call on app startup.
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureDatabaseCreated()
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.IO;
|
||||||
|
namespace UtopiaCanteenSystem.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for creating AppDbContext instances. Used by RfidService and SyncService
|
||||||
|
/// to avoid sharing a single context across async operations.
|
||||||
|
/// </summary>
|
||||||
|
public class DbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
|
{
|
||||||
|
private static readonly DbContextOptions<AppDbContext> Options;
|
||||||
|
|
||||||
|
static DbContextFactory()
|
||||||
|
{
|
||||||
|
var builder = new DbContextOptionsBuilder<AppDbContext>();
|
||||||
|
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "utopia_canteen.db");
|
||||||
|
builder.UseSqlite($"Data Source={dbPath}");
|
||||||
|
Options = builder.Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppDbContext CreateDbContext() => new AppDbContext(Options);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for scan session rules: limit one scan per minute (1-minute session).
|
||||||
|
/// </summary>
|
||||||
|
public static class ScanSessionHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a new scan is allowed: no other scan in the same minute (UTC).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lastScanTimeUtc">Last scan time in UTC; null if none.</param>
|
||||||
|
/// <param name="nowUtc">Current time in UTC.</param>
|
||||||
|
/// <returns>True if a new scan is allowed.</returns>
|
||||||
|
public static bool IsNewScanAllowed(DateTime? lastScanTimeUtc, DateTime nowUtc)
|
||||||
|
{
|
||||||
|
if (lastScanTimeUtc == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Same minute (year, month, day, hour, minute) = not allowed
|
||||||
|
var last = lastScanTimeUtc.Value;
|
||||||
|
if (last.Year == nowUtc.Year &&
|
||||||
|
last.Month == nowUtc.Month &&
|
||||||
|
last.Day == nowUtc.Day &&
|
||||||
|
last.Hour == nowUtc.Hour &&
|
||||||
|
last.Minute == nowUtc.Minute)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Truncates the given UTC time to the start of its minute (session window).
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime TruncateToMinute(DateTime utc)
|
||||||
|
{
|
||||||
|
return new DateTime(utc.Year, utc.Month, utc.Day, utc.Hour, utc.Minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Window x:Class="UtopiaCanteenSystem.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Utopia Canteen System"
|
||||||
|
MinHeight="600" MinWidth="800"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
SizeToContent="WidthAndHeight">
|
||||||
|
<Grid>
|
||||||
|
<ContentControl Content="{Binding CurrentViewModel}" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main window: hosts the current view via ContentControl and DataTemplates.
|
||||||
|
/// DataContext is set from App.xaml.cs to MainViewModel. No logic in code-behind.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace UtopiaCanteenSystem.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a labour/worker entity. CardId is the primary identifier from RFID.
|
||||||
|
/// </summary>
|
||||||
|
public class Labour
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
/// <summary>RFID card identifier; used for scan matching.</summary>
|
||||||
|
public string CardId { get; set; } = string.Empty;
|
||||||
|
/// <summary>Optional display name for the labour.</summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace UtopiaCanteenSystem.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single RFID scan event. Used for local storage and UIND sync.
|
||||||
|
/// </summary>
|
||||||
|
public class ScanRecord
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
/// <summary>RFID card identifier at time of scan.</summary>
|
||||||
|
public string CardId { get; set; } = string.Empty;
|
||||||
|
/// <summary>UTC time when the scan occurred.</summary>
|
||||||
|
public DateTime ScanTime { get; set; }
|
||||||
|
/// <summary>True after record has been successfully sent to the sync API.</summary>
|
||||||
|
public bool IsSynced { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the current admin session state for the app.
|
||||||
|
/// </summary>
|
||||||
|
public class AppSession
|
||||||
|
{
|
||||||
|
public bool IsAdminAuthenticated { get; private set; }
|
||||||
|
public string AdminUsername { get; private set; } = string.Empty;
|
||||||
|
public string AdminEmployeeId { get; private set; } = string.Empty;
|
||||||
|
public DateTime? LoginTimeUtc { get; private set; }
|
||||||
|
|
||||||
|
public void SetAdminAuthenticated(string username, string employeeId)
|
||||||
|
{
|
||||||
|
IsAdminAuthenticated = true;
|
||||||
|
AdminUsername = username ?? string.Empty;
|
||||||
|
AdminEmployeeId = employeeId ?? string.Empty;
|
||||||
|
LoginTimeUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Logout()
|
||||||
|
{
|
||||||
|
IsAdminAuthenticated = false;
|
||||||
|
AdminUsername = string.Empty;
|
||||||
|
AdminEmployeeId = string.Empty;
|
||||||
|
LoginTimeUtc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of an admin authentication attempt.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct AuthResult(
|
||||||
|
bool Success,
|
||||||
|
string EmployeeId);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Admin authentication service via UIND portal API (plain-text response).
|
||||||
|
/// </summary>
|
||||||
|
public class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private static readonly HttpClient HttpClient = new();
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public AuthService(string authenticationUrl)
|
||||||
|
{
|
||||||
|
_baseUrl = (authenticationUrl ?? string.Empty).Trim();
|
||||||
|
if (!string.IsNullOrEmpty(_baseUrl) && !_baseUrl.EndsWith("/"))
|
||||||
|
_baseUrl += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AuthResult> LoginAsync(string username, string password, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var user = username?.Trim() ?? string.Empty;
|
||||||
|
var pass = password ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(_baseUrl))
|
||||||
|
return new AuthResult(false, string.Empty);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(user) || string.IsNullOrEmpty(pass))
|
||||||
|
return new AuthResult(false, string.Empty);
|
||||||
|
|
||||||
|
// Build URL:
|
||||||
|
// {baseUrl}{username}/{password}
|
||||||
|
// Must URL-encode both values.
|
||||||
|
var url = $"{_baseUrl}{Uri.EscapeDataString(user)}/{Uri.EscapeDataString(pass)}";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var resp = await HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
var body = (await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)).Trim();
|
||||||
|
|
||||||
|
// Portal returns plain text:
|
||||||
|
// "NOT FOUND" => failure
|
||||||
|
// otherwise => success and body contains EmployeeId (or user identifier)
|
||||||
|
if (body.Equals("NOT FOUND", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return new AuthResult(false, string.Empty);
|
||||||
|
|
||||||
|
return new AuthResult(true, body);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new AuthResult(false, string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Simple JSON-backed configuration stored alongside the app binaries.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigService : IConfigService
|
||||||
|
{
|
||||||
|
private readonly string _configPath;
|
||||||
|
private AppConfig _config;
|
||||||
|
|
||||||
|
public ConfigService()
|
||||||
|
{
|
||||||
|
_configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||||
|
_config = LoadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetSyncApiEndpoint() => _config.SyncApiEndpoint ?? string.Empty;
|
||||||
|
|
||||||
|
public void SetSyncApiEndpoint(string endpoint)
|
||||||
|
{
|
||||||
|
_config.SyncApiEndpoint = endpoint ?? string.Empty;
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool GetScannerConnected() => _config.ScannerConnected;
|
||||||
|
|
||||||
|
public void SetScannerConnected(bool connected)
|
||||||
|
{
|
||||||
|
_config.ScannerConnected = connected;
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetScanTimeoutSeconds() => _config.ScanTimeoutSeconds;
|
||||||
|
|
||||||
|
public void SetScanTimeoutSeconds(int seconds)
|
||||||
|
{
|
||||||
|
_config.ScanTimeoutSeconds = seconds;
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetAdminCardId() => _config.AdminCardId ?? string.Empty;
|
||||||
|
|
||||||
|
public void SetAdminCardId(string cardId)
|
||||||
|
{
|
||||||
|
_config.AdminCardId = cardId ?? string.Empty;
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppConfig LoadConfig()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_configPath))
|
||||||
|
{
|
||||||
|
var defaults = new AppConfig();
|
||||||
|
SaveConfig(defaults);
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(_configPath);
|
||||||
|
var config = JsonSerializer.Deserialize<AppConfig>(json);
|
||||||
|
return config ?? new AppConfig();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new AppConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveConfig()
|
||||||
|
{
|
||||||
|
SaveConfig(_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveConfig(AppConfig config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
File.WriteAllText(_configPath, json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore write failures (e.g., read-only location).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AppConfig
|
||||||
|
{
|
||||||
|
public string SyncApiEndpoint { get; set; } = "https://api.example.com/uind/sync";
|
||||||
|
public bool ScannerConnected { get; set; } = false;
|
||||||
|
public int ScanTimeoutSeconds { get; set; } = 60;
|
||||||
|
public string AdminCardId { get; set; } = "ADMIN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
Task<AuthResult> LoginAsync(string username, string password, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides persisted configuration values for the app.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigService
|
||||||
|
{
|
||||||
|
string GetSyncApiEndpoint();
|
||||||
|
void SetSyncApiEndpoint(string endpoint);
|
||||||
|
bool GetScannerConnected();
|
||||||
|
void SetScannerConnected(bool connected);
|
||||||
|
int GetScanTimeoutSeconds();
|
||||||
|
void SetScanTimeoutSeconds(int seconds);
|
||||||
|
string GetAdminCardId();
|
||||||
|
void SetAdminCardId(string cardId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigates between main views (AdminLogin, Scanner, Dashboard, Settings).
|
||||||
|
/// </summary>
|
||||||
|
public interface INavigationService
|
||||||
|
{
|
||||||
|
/// <summary>Current view model to display in the main content area.</summary>
|
||||||
|
object? CurrentViewModel { get; }
|
||||||
|
|
||||||
|
/// <summary>Raised when CurrentViewModel changes.</summary>
|
||||||
|
event EventHandler? CurrentViewModelChanged;
|
||||||
|
|
||||||
|
void NavigateToAdminLogin();
|
||||||
|
void NavigateToScanner();
|
||||||
|
void NavigateToDashboard();
|
||||||
|
void NavigateToAdminSettingsAuth();
|
||||||
|
void NavigateToSettings();
|
||||||
|
|
||||||
|
/// <summary>Starts a new dashboard session (used after scan).</summary>
|
||||||
|
void StartDashboardSession();
|
||||||
|
|
||||||
|
/// <summary>Returns true when the dashboard session is expired for the given timeout.</summary>
|
||||||
|
bool IsDashboardSessionExpired(TimeSpan timeout);
|
||||||
|
|
||||||
|
/// <summary>Navigates back from Settings based on dashboard session expiry.</summary>
|
||||||
|
void NavigateBackFromSettings(TimeSpan timeout);
|
||||||
|
|
||||||
|
/// <summary>Returns elapsed time since dashboard session start.</summary>
|
||||||
|
TimeSpan GetDashboardSessionElapsed();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UtopiaCanteenSystem.Models;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles RFID scan logic: validation, timeout rules, and persisting scan records.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRfidService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a scan for the given card ID. Applies configured timeout,
|
||||||
|
/// saves to ScanRecords if allowed, and returns result message.
|
||||||
|
/// </summary>
|
||||||
|
(bool Success, string Message) ProcessScan(string cardId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a scan and also returns cooldown seconds remaining when blocked by timeout rules.
|
||||||
|
/// </summary>
|
||||||
|
ScanResult ProcessScanDetailed(string cardId);
|
||||||
|
|
||||||
|
/// <summary>Returns the last scan record for display (e.g. dashboard).</summary>
|
||||||
|
ScanRecord? GetLastScan();
|
||||||
|
|
||||||
|
/// <summary>Returns the count of scans recorded today (local date).</summary>
|
||||||
|
int GetTodayScanCount();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the count of scans recorded today (local date boundaries), asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns total number of scans recorded for a given card ID (all time).
|
||||||
|
/// Used to detect repeat scans by the same user.
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns number of scans recorded today (local day) for a given card ID.
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetTodayScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Syncs unsynced ScanRecords to the configured UIND API endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISyncService
|
||||||
|
{
|
||||||
|
/// <summary>Runs one sync: POST unsynced records to API and mark as synced on success.</summary>
|
||||||
|
Task SyncNowAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
using UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the current view model and notifies when it changes for binding in MainWindow.
|
||||||
|
/// </summary>
|
||||||
|
public class NavigationService : INavigationService
|
||||||
|
{
|
||||||
|
private object? _currentViewModel;
|
||||||
|
private DateTime? _dashboardSessionStartUtc;
|
||||||
|
private readonly AppSession _session;
|
||||||
|
|
||||||
|
public object? CurrentViewModel
|
||||||
|
{
|
||||||
|
get => _currentViewModel;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (_currentViewModel == value) return;
|
||||||
|
_currentViewModel = value;
|
||||||
|
CurrentViewModelChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CurrentViewModelChanged;
|
||||||
|
|
||||||
|
private readonly Func<AdminLoginViewModel> _adminLoginVm;
|
||||||
|
private readonly Func<ScannerViewModel> _scannerVm;
|
||||||
|
private readonly Func<MainDashboardViewModel> _dashboardVm;
|
||||||
|
private readonly Func<AdminSettingsAuthViewModel> _adminSettingsAuthVm;
|
||||||
|
private readonly Func<SettingsViewModel> _settingsVm;
|
||||||
|
|
||||||
|
public NavigationService(
|
||||||
|
AppSession session,
|
||||||
|
Func<AdminLoginViewModel> adminLoginVm,
|
||||||
|
Func<ScannerViewModel> scannerVm,
|
||||||
|
Func<MainDashboardViewModel> dashboardVm,
|
||||||
|
Func<AdminSettingsAuthViewModel> adminSettingsAuthVm,
|
||||||
|
Func<SettingsViewModel> settingsVm)
|
||||||
|
{
|
||||||
|
_session = session;
|
||||||
|
_adminLoginVm = adminLoginVm;
|
||||||
|
_scannerVm = scannerVm;
|
||||||
|
_dashboardVm = dashboardVm;
|
||||||
|
_adminSettingsAuthVm = adminSettingsAuthVm;
|
||||||
|
_settingsVm = settingsVm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NavigateToAdminLogin()
|
||||||
|
{
|
||||||
|
CurrentViewModel = _adminLoginVm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NavigateToScanner()
|
||||||
|
{
|
||||||
|
if (!_session.IsAdminAuthenticated)
|
||||||
|
{
|
||||||
|
NavigateToAdminLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentViewModel = _scannerVm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NavigateToDashboard()
|
||||||
|
{
|
||||||
|
if (!_session.IsAdminAuthenticated)
|
||||||
|
{
|
||||||
|
NavigateToAdminLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentViewModel = _dashboardVm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NavigateToSettings()
|
||||||
|
{
|
||||||
|
if (!_session.IsAdminAuthenticated)
|
||||||
|
{
|
||||||
|
NavigateToAdminLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentViewModel = _settingsVm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NavigateToAdminSettingsAuth()
|
||||||
|
{
|
||||||
|
if (!_session.IsAdminAuthenticated)
|
||||||
|
{
|
||||||
|
NavigateToAdminLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentViewModel = _adminSettingsAuthVm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartDashboardSession()
|
||||||
|
{
|
||||||
|
_dashboardSessionStartUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDashboardSessionExpired(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (!_dashboardSessionStartUtc.HasValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return DateTime.UtcNow - _dashboardSessionStartUtc.Value >= timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan GetDashboardSessionElapsed()
|
||||||
|
{
|
||||||
|
if (!_dashboardSessionStartUtc.HasValue)
|
||||||
|
return TimeSpan.MaxValue;
|
||||||
|
|
||||||
|
return DateTime.UtcNow - _dashboardSessionStartUtc.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NavigateBackFromSettings(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (IsDashboardSessionExpired(timeout))
|
||||||
|
NavigateToScanner();
|
||||||
|
else
|
||||||
|
NavigateToDashboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UtopiaCanteenSystem.Data;
|
||||||
|
using UtopiaCanteenSystem.Helpers;
|
||||||
|
using UtopiaCanteenSystem.Models;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles RFID scan logic: validates input, enforces configurable timeout,
|
||||||
|
/// and saves ScanRecord to SQLite.
|
||||||
|
/// </summary>
|
||||||
|
public class RfidService : IRfidService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
|
||||||
|
public RfidService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (bool Success, string Message) ProcessScan(string cardId)
|
||||||
|
{
|
||||||
|
var result = ProcessScanDetailed(cardId);
|
||||||
|
return (result.Success, result.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScanResult ProcessScanDetailed(string cardId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cardId))
|
||||||
|
return new ScanResult(false, "Card ID cannot be empty.", 0);
|
||||||
|
|
||||||
|
cardId = cardId.Trim();
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
using var db = _dbFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var timeoutSeconds = _configService.GetScanTimeoutSeconds();
|
||||||
|
if (timeoutSeconds <= 0)
|
||||||
|
timeoutSeconds = 60;
|
||||||
|
|
||||||
|
var windowStart = nowUtc.AddSeconds(-timeoutSeconds);
|
||||||
|
|
||||||
|
// Enforce timeout: no duplicate scan within the timeout window
|
||||||
|
var lastInWindow = db.ScanRecords
|
||||||
|
.Where(r => r.CardId == cardId)
|
||||||
|
.Where(r => r.ScanTime >= windowStart)
|
||||||
|
.OrderByDescending(r => r.ScanTime)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (lastInWindow != null)
|
||||||
|
{
|
||||||
|
var remaining = GetCooldownRemainingSeconds(nowUtc, lastInWindow.ScanTime, timeoutSeconds);
|
||||||
|
return new ScanResult(
|
||||||
|
false,
|
||||||
|
$"Multiple scans within {FormatTimeout(timeoutSeconds)} are not allowed. Please wait.",
|
||||||
|
remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: prevent any scan within the timeout window (any card).
|
||||||
|
var lastAnyScanInWindow = db.ScanRecords
|
||||||
|
.Where(r => r.ScanTime >= windowStart)
|
||||||
|
.OrderByDescending(r => r.ScanTime)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (lastAnyScanInWindow != null)
|
||||||
|
{
|
||||||
|
var remaining = GetCooldownRemainingSeconds(nowUtc, lastAnyScanInWindow.ScanTime, timeoutSeconds);
|
||||||
|
return new ScanResult(
|
||||||
|
false,
|
||||||
|
$"Only one scan within {FormatTimeout(timeoutSeconds)} is allowed. Please wait.",
|
||||||
|
remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = new ScanRecord
|
||||||
|
{
|
||||||
|
CardId = cardId,
|
||||||
|
ScanTime = nowUtc,
|
||||||
|
IsSynced = false
|
||||||
|
};
|
||||||
|
db.ScanRecords.Add(record);
|
||||||
|
db.SaveChanges();
|
||||||
|
|
||||||
|
return new ScanResult(true, "Scan recorded successfully.", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScanRecord? GetLastScan()
|
||||||
|
{
|
||||||
|
using var db = _dbFactory.CreateDbContext();
|
||||||
|
return db.ScanRecords
|
||||||
|
.OrderByDescending(r => r.ScanTime)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetTodayScanCount()
|
||||||
|
{
|
||||||
|
// Define "today" by the local calendar day, but ScanTime is stored as UTC.
|
||||||
|
// Convert local day boundaries to UTC for correct comparisons.
|
||||||
|
var startOfTodayLocal = DateTime.Today;
|
||||||
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
||||||
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
||||||
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||||
|
|
||||||
|
using var db = _dbFactory.CreateDbContext();
|
||||||
|
return db.ScanRecords
|
||||||
|
.Count(r => r.ScanTime >= startUtc && r.ScanTime < endUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTodayScanCountAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Define "today" by the local calendar day, but ScanTime is stored as UTC.
|
||||||
|
// Convert local day boundaries to UTC for correct comparisons.
|
||||||
|
var startOfTodayLocal = DateTime.Today;
|
||||||
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
||||||
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
||||||
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||||
|
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await db.ScanRecords
|
||||||
|
.CountAsync(r => r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cardId))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
cardId = cardId.Trim();
|
||||||
|
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await db.ScanRecords
|
||||||
|
.CountAsync(r => r.CardId == cardId, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTodayScanCountForCardAsync(string cardId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cardId))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
cardId = cardId.Trim();
|
||||||
|
|
||||||
|
// Define "today" by the local calendar day, but ScanTime is stored as UTC.
|
||||||
|
// Convert local day boundaries to UTC for correct comparisons.
|
||||||
|
var startOfTodayLocal = DateTime.Today;
|
||||||
|
var startOfTomorrowLocal = startOfTodayLocal.AddDays(1);
|
||||||
|
var startUtc = startOfTodayLocal.ToUniversalTime();
|
||||||
|
var endUtc = startOfTomorrowLocal.ToUniversalTime();
|
||||||
|
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await db.ScanRecords
|
||||||
|
.CountAsync(r => r.CardId == cardId && r.ScanTime >= startUtc && r.ScanTime < endUtc, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTimeout(int seconds)
|
||||||
|
{
|
||||||
|
if (seconds <= 0)
|
||||||
|
return "the timeout window";
|
||||||
|
|
||||||
|
var minutes = seconds / 60;
|
||||||
|
var secs = seconds % 60;
|
||||||
|
|
||||||
|
if (minutes == 0)
|
||||||
|
return secs == 1 ? "1 second" : $"{secs} seconds";
|
||||||
|
|
||||||
|
if (secs == 0)
|
||||||
|
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
||||||
|
|
||||||
|
var minutePart = minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
||||||
|
var secondPart = secs == 1 ? "1 second" : $"{secs} seconds";
|
||||||
|
return $"{minutePart} {secondPart}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetCooldownRemainingSeconds(DateTime nowUtc, DateTime lastScanUtc, int timeoutSeconds)
|
||||||
|
{
|
||||||
|
// Remaining = (lastScan + timeout) - now. Use ceiling so UI shows a whole-second countdown.
|
||||||
|
var endUtc = lastScanUtc.AddSeconds(timeoutSeconds);
|
||||||
|
var remaining = (int)Math.Ceiling((endUtc - nowUtc).TotalSeconds);
|
||||||
|
return Math.Max(0, remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// =============================================================================
|
||||||
|
// SAMPLE API POST ENDPOINT (for reference / testing)
|
||||||
|
// =============================================================================
|
||||||
|
// This file is NOT compiled; it shows how a server could accept the sync POST
|
||||||
|
// from UtopiaCanteenSystem. Add something like this to your UIND sync API.
|
||||||
|
//
|
||||||
|
// Expected request from UtopiaCanteenSystem:
|
||||||
|
// POST {SyncApiEndpoint} (e.g. https://api.example.com/uind/sync)
|
||||||
|
// Content-Type: application/json
|
||||||
|
// Body: array of scan records, e.g.:
|
||||||
|
// [
|
||||||
|
// { "Id": 1, "CardId": "RFID123", "ScanTime": "2025-01-30T10:00:00Z", "IsSynced": false },
|
||||||
|
// { "Id": 2, "CardId": "RFID456", "ScanTime": "2025-01-30T10:05:00Z", "IsSynced": false }
|
||||||
|
// ]
|
||||||
|
//
|
||||||
|
// Response: 2xx success → client will mark those records as IsSynced = true.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#if false // Sample ASP.NET Core controller (paste into your API project)
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace YourApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("uind/sync")]
|
||||||
|
public class UindSyncController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult Sync([FromBody] List<SyncRecordDto> records)
|
||||||
|
{
|
||||||
|
if (records == null || records.Count == 0)
|
||||||
|
return Ok();
|
||||||
|
|
||||||
|
// Persist or process records (e.g. save to your database)
|
||||||
|
foreach (var r in records)
|
||||||
|
{
|
||||||
|
// Save r.Id, r.CardId, r.ScanTime, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SyncRecordDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string CardId { get; set; } = string.Empty;
|
||||||
|
public DateTime ScanTime { get; set; }
|
||||||
|
public bool IsSynced { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of processing a scan, including an optional cooldown (in seconds) when blocked.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct ScanResult(
|
||||||
|
bool Success,
|
||||||
|
string Message,
|
||||||
|
int CooldownSecondsRemaining);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UtopiaCanteenSystem.Data;
|
||||||
|
using UtopiaCanteenSystem.Models;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hourly sync: fetches unsynced ScanRecords, POSTs them to the configured API,
|
||||||
|
/// and marks them as IsSynced on success.
|
||||||
|
/// </summary>
|
||||||
|
public class SyncService : ISyncService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private static readonly HttpClient HttpClient = new();
|
||||||
|
|
||||||
|
public SyncService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SyncNowAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var endpoint = _configService.GetSyncApiEndpoint();
|
||||||
|
if (string.IsNullOrWhiteSpace(endpoint))
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<ScanRecord> toSync;
|
||||||
|
using (var db = _dbFactory.CreateDbContext())
|
||||||
|
{
|
||||||
|
toSync = await db.ScanRecords
|
||||||
|
.Where(r => !r.IsSynced)
|
||||||
|
.OrderBy(r => r.ScanTime)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toSync.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var payload = toSync.Select(r => new
|
||||||
|
{
|
||||||
|
r.Id,
|
||||||
|
r.CardId,
|
||||||
|
ScanTime = r.ScanTime,
|
||||||
|
r.IsSynced
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await HttpClient
|
||||||
|
.PostAsJsonAsync(endpoint, payload, cancellationToken: cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ids = toSync.Select(r => r.Id).ToList();
|
||||||
|
using (var db = _dbFactory.CreateDbContext())
|
||||||
|
{
|
||||||
|
var records = await db.ScanRecords
|
||||||
|
.Where(r => ids.Contains(r.Id))
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
foreach (var r in records)
|
||||||
|
r.IsSynced = true;
|
||||||
|
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Leave records unsynced; will retry on next run
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ApplicationIcon></ApplicationIcon>
|
||||||
|
<RootNamespace>UtopiaCanteenSystem</RootNamespace>
|
||||||
|
<AssemblyName>UtopiaCanteenSystem</AssemblyName>
|
||||||
|
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
public partial class AdminLoginViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
private readonly AppSession _session;
|
||||||
|
private readonly INavigationService _navigation;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _username = string.Empty;
|
||||||
|
|
||||||
|
// Updated from code-behind (PasswordBox doesn't support binding cleanly).
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _password = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _errorMessage = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isBusy;
|
||||||
|
|
||||||
|
public AdminLoginViewModel(IAuthService authService, AppSession session, INavigationService navigation)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
_session = session;
|
||||||
|
_navigation = navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AdminLoginAsync()
|
||||||
|
{
|
||||||
|
if (IsBusy)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ErrorMessage = string.Empty;
|
||||||
|
|
||||||
|
var user = Username?.Trim() ?? string.Empty;
|
||||||
|
var pass = Password ?? string.Empty;
|
||||||
|
|
||||||
|
// Requirement: after clicking login, clear fields.
|
||||||
|
Username = string.Empty;
|
||||||
|
Password = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(user) || string.IsNullOrEmpty(pass))
|
||||||
|
{
|
||||||
|
ErrorMessage = "Username and password are required.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _authService.LoginAsync(user, pass).ConfigureAwait(true);
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
ErrorMessage = "Invalid username or password.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_session.SetAdminAuthenticated(user, result.EmployeeId);
|
||||||
|
_navigation.NavigateToScanner();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
public partial class AdminSettingsAuthViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
private readonly AppSession _session;
|
||||||
|
private readonly INavigationService _navigation;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _username = string.Empty;
|
||||||
|
|
||||||
|
// Updated from code-behind (PasswordBox doesn't support binding cleanly).
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _password = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _errorMessage = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isBusy;
|
||||||
|
|
||||||
|
public AdminSettingsAuthViewModel(IAuthService authService, AppSession session, INavigationService navigation)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
_session = session;
|
||||||
|
_navigation = navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
_navigation.NavigateToDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ConfirmAsync()
|
||||||
|
{
|
||||||
|
if (IsBusy)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ErrorMessage = string.Empty;
|
||||||
|
|
||||||
|
var user = Username?.Trim() ?? string.Empty;
|
||||||
|
var pass = Password ?? string.Empty;
|
||||||
|
|
||||||
|
// Clear fields after clicking confirm (per kiosk behavior).
|
||||||
|
Username = string.Empty;
|
||||||
|
Password = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(user) || string.IsNullOrEmpty(pass))
|
||||||
|
{
|
||||||
|
ErrorMessage = "Admin username and password are required.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _authService.LoginAsync(user, pass).ConfigureAwait(true);
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
ErrorMessage = "Admin rights required. Invalid username/password.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh session details (who authenticated for Settings).
|
||||||
|
_session.SetAdminAuthenticated(user, result.EmployeeId);
|
||||||
|
_navigation.NavigateToSettings();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for MainDashboardView: shows last scanned card ID, date/time, scan count, and success/error message.
|
||||||
|
/// Manages a configurable session timer to navigate back to login.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainDashboardViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly INavigationService _navigation;
|
||||||
|
private readonly IRfidService _rfidService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly AppSession _session;
|
||||||
|
private CancellationTokenSource? _timerCancellation;
|
||||||
|
private readonly DispatcherTimer _clockTimer;
|
||||||
|
private readonly DispatcherTimer _todayCountTimer;
|
||||||
|
private DateTime _lastCountDate = DateTime.Today;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _lastCardId = "—";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _lastScanTime = "—";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private int _scanCount;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _statusMessage = "Scan recorded successfully.";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _currentTime = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isSuccess = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _canScan = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isAdmin;
|
||||||
|
|
||||||
|
public MainDashboardViewModel(
|
||||||
|
INavigationService navigation,
|
||||||
|
IRfidService rfidService,
|
||||||
|
IConfigService configService,
|
||||||
|
AppSession session)
|
||||||
|
{
|
||||||
|
_navigation = navigation;
|
||||||
|
_rfidService = rfidService;
|
||||||
|
_configService = configService;
|
||||||
|
_session = session;
|
||||||
|
IsAdmin = _session.IsAdminAuthenticated;
|
||||||
|
UpdateCurrentTime();
|
||||||
|
_clockTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_clockTimer.Tick += (_, _) => UpdateCurrentTime();
|
||||||
|
_clockTimer.Start();
|
||||||
|
UpdateScanDetails();
|
||||||
|
|
||||||
|
// Refresh "today's scans" periodically so it auto-resets after midnight even without scans.
|
||||||
|
_todayCountTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
_todayCountTimer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
if (DateTime.Today == _lastCountDate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_lastCountDate = DateTime.Today;
|
||||||
|
_ = LoadTodayScanCountAsync();
|
||||||
|
};
|
||||||
|
_todayCountTimer.Start();
|
||||||
|
|
||||||
|
// Initial load of today's count (async).
|
||||||
|
_ = LoadTodayScanCountAsync();
|
||||||
|
|
||||||
|
StartDashboardTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates scan details: last scan info, scan count for today, and last scan time.
|
||||||
|
/// Call this after a new scan occurs.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateScanDetails()
|
||||||
|
{
|
||||||
|
var last = _rfidService.GetLastScan();
|
||||||
|
if (last != null)
|
||||||
|
{
|
||||||
|
LastCardId = last.CardId;
|
||||||
|
LastScanTime = last.ScanTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
// Default success message; may be replaced with "welcome back" if repeat detected.
|
||||||
|
IsSuccess = true;
|
||||||
|
StatusMessage = "Scan recorded successfully.";
|
||||||
|
_ = UpdateRepeatMessageAsync(last.CardId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LastCardId = "—";
|
||||||
|
LastScanTime = "—";
|
||||||
|
IsSuccess = true;
|
||||||
|
StatusMessage = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateRepeatMessageAsync(string cardId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var todayCount = await _rfidService.GetTodayScanCountForCardAsync(cardId).ConfigureAwait(false);
|
||||||
|
var totalCount = await _rfidService.GetTotalScanCountForCardAsync(cardId).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Only show the "came again" message when we have evidence of a prior scan.
|
||||||
|
if (totalCount >= 2)
|
||||||
|
{
|
||||||
|
var msg =
|
||||||
|
todayCount >= 2
|
||||||
|
? $"Welcome back! You scanned again (#{todayCount} today)."
|
||||||
|
: "Welcome back! You scanned again.";
|
||||||
|
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
IsSuccess = true;
|
||||||
|
StatusMessage = msg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If DB query fails, keep the default status message.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadTodayScanCountAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Counts only scans for the current LOCAL day (converted to UTC boundaries internally).
|
||||||
|
var count = await _rfidService.GetTodayScanCountAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Ensure property changes happen on the UI thread.
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() => ScanCount = count);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If DB is unavailable for any reason, fail gracefully.
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() => ScanCount = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the dashboard session timer. When the session expires, navigates back to login.
|
||||||
|
/// </summary>
|
||||||
|
private void StartDashboardTimer()
|
||||||
|
{
|
||||||
|
// Cancel any existing timer
|
||||||
|
_timerCancellation?.Cancel();
|
||||||
|
_timerCancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// For the session, restrict scans to current employee
|
||||||
|
CanScan = true; // Current employee can scan
|
||||||
|
|
||||||
|
var timeout = GetDashboardTimeout();
|
||||||
|
if (_navigation.IsDashboardSessionExpired(timeout))
|
||||||
|
{
|
||||||
|
_navigation.NavigateToScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use remaining time since the dashboard session started.
|
||||||
|
var remaining = timeout - _navigation.GetDashboardSessionElapsed();
|
||||||
|
|
||||||
|
if (remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
_navigation.NavigateToScanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start async timer
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(remaining, _timerCancellation.Token);
|
||||||
|
|
||||||
|
// Timer completed: navigate back to login
|
||||||
|
// Use Dispatcher to ensure UI thread
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_navigation.CurrentViewModel, this))
|
||||||
|
_navigation.NavigateToScanner();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Timer was cancelled (new scan occurred, timer reset)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a new scan occurs.
|
||||||
|
/// </summary>
|
||||||
|
public void OnScanOccurred()
|
||||||
|
{
|
||||||
|
UpdateScanDetails();
|
||||||
|
_lastCountDate = DateTime.Today;
|
||||||
|
_ = LoadTodayScanCountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void BackToLogin()
|
||||||
|
{
|
||||||
|
_timerCancellation?.Cancel();
|
||||||
|
_navigation.NavigateToScanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void GoToSettings()
|
||||||
|
{
|
||||||
|
// Require admin credentials at time of access (kiosk mode).
|
||||||
|
_navigation.NavigateToAdminSettingsAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleanup: cancel timer when ViewModel is disposed or view is closed.
|
||||||
|
/// </summary>
|
||||||
|
~MainDashboardViewModel()
|
||||||
|
{
|
||||||
|
_clockTimer.Stop();
|
||||||
|
_todayCountTimer.Stop();
|
||||||
|
_timerCancellation?.Cancel();
|
||||||
|
_timerCancellation?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan GetDashboardTimeout()
|
||||||
|
{
|
||||||
|
var seconds = _configService.GetScanTimeoutSeconds();
|
||||||
|
if (seconds <= 0)
|
||||||
|
seconds = 60;
|
||||||
|
return TimeSpan.FromSeconds(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCurrentTime()
|
||||||
|
{
|
||||||
|
CurrentTime = DateTime.Now.ToString("MM/dd/yyyy, hh:mm:ss tt");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main window ViewModel: exposes current view model from navigation for ContentControl binding.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
private object? _currentViewModel;
|
||||||
|
|
||||||
|
public INavigationService Navigation { get; }
|
||||||
|
|
||||||
|
public MainViewModel(INavigationService navigation)
|
||||||
|
{
|
||||||
|
Navigation = navigation;
|
||||||
|
Navigation.CurrentViewModelChanged += (_, _) => CurrentViewModel = Navigation.CurrentViewModel;
|
||||||
|
Navigation.NavigateToAdminLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
using DebounceTimer = System.Timers.Timer;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for ScannerView: RFID input, scan command, scanner status, current scan time.
|
||||||
|
/// Supports both Enter-to-submit and debounce-to-submit for keyboard-wedge scanners.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ScannerViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private const int DebounceMs = 200;
|
||||||
|
private const int CooldownTickSeconds = 1;
|
||||||
|
private static readonly TimeSpan ScannerInactivityTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
private readonly IRfidService _rfidService;
|
||||||
|
private readonly INavigationService _navigation;
|
||||||
|
private readonly AppSession _session;
|
||||||
|
private readonly Dispatcher _uiDispatcher;
|
||||||
|
private readonly DebounceTimer _debounceTimer;
|
||||||
|
private readonly DispatcherTimer _cooldownTimer;
|
||||||
|
private DateTime? _cooldownEndsUtc;
|
||||||
|
private int _lastDisplayedCooldownSeconds = -1;
|
||||||
|
private readonly DispatcherTimer _scannerStatusTimer;
|
||||||
|
private DateTime? _lastScanActivityUtc;
|
||||||
|
private readonly object _submitLock = new();
|
||||||
|
private bool _isSubmitting;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsWatermarkVisible))]
|
||||||
|
private string _cardIdInput = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>True when CardIdInput is empty; used to show/hide the "Scan Card ID" watermark.</summary>
|
||||||
|
public bool IsWatermarkVisible => string.IsNullOrWhiteSpace(CardIdInput);
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ScannerStatusDisplay))]
|
||||||
|
private string _scannerStatus = "Disconnected";
|
||||||
|
|
||||||
|
public string ScannerStatusDisplay =>
|
||||||
|
string.Equals(ScannerStatus, "Connected", StringComparison.Ordinal) ? "Connected" : "waiting for scan...";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _currentScanTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _message = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isSuccess;
|
||||||
|
|
||||||
|
public ScannerViewModel(IRfidService rfidService, INavigationService navigation, AppSession session)
|
||||||
|
{
|
||||||
|
_rfidService = rfidService;
|
||||||
|
_navigation = navigation;
|
||||||
|
_session = session;
|
||||||
|
|
||||||
|
_uiDispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||||
|
_debounceTimer = new DebounceTimer(DebounceMs)
|
||||||
|
{
|
||||||
|
AutoReset = false
|
||||||
|
};
|
||||||
|
_debounceTimer.Elapsed += (_, _) =>
|
||||||
|
{
|
||||||
|
// Timer runs on a background thread; marshal back to UI thread.
|
||||||
|
_uiDispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
lock (_submitLock)
|
||||||
|
{
|
||||||
|
if (_isSubmitting)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If something already cleared the input (e.g., Enter submit), do nothing.
|
||||||
|
if (string.IsNullOrWhiteSpace(CardIdInput))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ScanCommand.CanExecute(null))
|
||||||
|
ScanCommand.Execute(null);
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
};
|
||||||
|
|
||||||
|
_cooldownTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(CooldownTickSeconds)
|
||||||
|
};
|
||||||
|
_cooldownTimer.Tick += (_, _) => UpdateCooldownMessage();
|
||||||
|
|
||||||
|
_scannerStatusTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_scannerStatusTimer.Tick += (_, _) => UpdateScannerStatusFromActivity();
|
||||||
|
_scannerStatusTimer.Start();
|
||||||
|
|
||||||
|
RefreshScannerStatus();
|
||||||
|
RefreshTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnCardIdInputChanged(string value)
|
||||||
|
{
|
||||||
|
// Clear any previous message once a new scan starts.
|
||||||
|
if (!string.IsNullOrWhiteSpace(value) && !string.IsNullOrEmpty(Message))
|
||||||
|
Message = string.Empty;
|
||||||
|
|
||||||
|
// If the user starts scanning again, don't keep overwriting their feedback with an old countdown.
|
||||||
|
StopCooldownCountdown();
|
||||||
|
|
||||||
|
// Keyboard-wedge scanners "type" into the TextBox. Any incoming characters means activity/connected.
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
_lastScanActivityUtc = DateTime.UtcNow;
|
||||||
|
ScannerStatus = "Connected";
|
||||||
|
}
|
||||||
|
|
||||||
|
RestartDebounceTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestartDebounceTimer()
|
||||||
|
{
|
||||||
|
_debounceTimer.Stop();
|
||||||
|
|
||||||
|
// Only auto-submit when we have a non-empty value.
|
||||||
|
if (string.IsNullOrWhiteSpace(CardIdInput))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_debounceTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshScannerStatus()
|
||||||
|
{
|
||||||
|
// Default: waiting for activity.
|
||||||
|
ScannerStatus = "Disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshTime()
|
||||||
|
{
|
||||||
|
CurrentScanTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Logout()
|
||||||
|
{
|
||||||
|
_session.Logout();
|
||||||
|
_navigation.NavigateToAdminLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Processes the current CardIdInput as a scan and navigates to dashboard on success.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void Scan()
|
||||||
|
{
|
||||||
|
lock (_submitLock)
|
||||||
|
{
|
||||||
|
if (_isSubmitting)
|
||||||
|
return;
|
||||||
|
_isSubmitting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure any pending debounce doesn't fire after an explicit submit.
|
||||||
|
_debounceTimer.Stop();
|
||||||
|
StopCooldownCountdown();
|
||||||
|
|
||||||
|
Message = string.Empty;
|
||||||
|
IsSuccess = false;
|
||||||
|
|
||||||
|
// Keep CardId as string (preserves leading zeros).
|
||||||
|
var cardId = CardIdInput?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
// Avoid showing "empty" errors if something triggered submit after we already cleared input.
|
||||||
|
if (string.IsNullOrWhiteSpace(cardId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var result = _rfidService.ProcessScanDetailed(cardId);
|
||||||
|
IsSuccess = result.Success;
|
||||||
|
Message = result.Message;
|
||||||
|
|
||||||
|
// Always clear input after attempt so the next scan starts cleanly.
|
||||||
|
CardIdInput = string.Empty;
|
||||||
|
|
||||||
|
if (!result.Success && result.CooldownSecondsRemaining > 0)
|
||||||
|
{
|
||||||
|
StartCooldownCountdown(result.CooldownSecondsRemaining);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
_navigation.StartDashboardSession();
|
||||||
|
_navigation.NavigateToDashboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_submitLock)
|
||||||
|
{
|
||||||
|
_isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartCooldownCountdown(int seconds)
|
||||||
|
{
|
||||||
|
if (seconds <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_cooldownEndsUtc = DateTime.UtcNow.AddSeconds(seconds);
|
||||||
|
_lastDisplayedCooldownSeconds = -1;
|
||||||
|
UpdateCooldownMessage(); // immediate update
|
||||||
|
_cooldownTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopCooldownCountdown()
|
||||||
|
{
|
||||||
|
_cooldownTimer.Stop();
|
||||||
|
_cooldownEndsUtc = null;
|
||||||
|
_lastDisplayedCooldownSeconds = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCooldownMessage()
|
||||||
|
{
|
||||||
|
if (_cooldownEndsUtc is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var remaining = (int)Math.Ceiling((_cooldownEndsUtc.Value - DateTime.UtcNow).TotalSeconds);
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
StopCooldownCountdown();
|
||||||
|
Message = "You can scan now.";
|
||||||
|
IsSuccess = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid redundant PropertyChanged churn.
|
||||||
|
if (remaining == _lastDisplayedCooldownSeconds)
|
||||||
|
return;
|
||||||
|
_lastDisplayedCooldownSeconds = remaining;
|
||||||
|
|
||||||
|
var unit = remaining == 1 ? "second" : "seconds";
|
||||||
|
Message = $"Please wait {remaining} {unit}…";
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateScannerStatusFromActivity()
|
||||||
|
{
|
||||||
|
if (_lastScanActivityUtc is null)
|
||||||
|
{
|
||||||
|
if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
|
||||||
|
ScannerStatus = "Disconnected";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inactiveFor = DateTime.UtcNow - _lastScanActivityUtc.Value;
|
||||||
|
if (inactiveFor > ScannerInactivityTimeout)
|
||||||
|
{
|
||||||
|
if (!string.Equals(ScannerStatus, "Disconnected", StringComparison.Ordinal))
|
||||||
|
ScannerStatus = "Disconnected";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScannerViewModel()
|
||||||
|
{
|
||||||
|
_scannerStatusTimer.Stop();
|
||||||
|
_cooldownTimer.Stop();
|
||||||
|
_debounceTimer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for SettingsView: editable API endpoint (UIND sync URL), save, load from config.
|
||||||
|
/// </summary>
|
||||||
|
public partial class SettingsViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly INavigationService _navigation;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _syncApiEndpoint = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _scanTimeoutSeconds = "30";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _adminCardId = "ADMIN";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _saveMessage = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isError;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isSaving;
|
||||||
|
|
||||||
|
public SettingsViewModel(IConfigService configService, INavigationService navigation)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_navigation = navigation;
|
||||||
|
LoadFromConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Loads SyncApiEndpoint from config service.</summary>
|
||||||
|
public void LoadFromConfig()
|
||||||
|
{
|
||||||
|
SyncApiEndpoint = _configService.GetSyncApiEndpoint();
|
||||||
|
ScanTimeoutSeconds = _configService.GetScanTimeoutSeconds().ToString();
|
||||||
|
AdminCardId = _configService.GetAdminCardId();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
IsSaving = true;
|
||||||
|
if (!int.TryParse(ScanTimeoutSeconds, out var seconds) || seconds <= 0)
|
||||||
|
{
|
||||||
|
SaveMessage = "Scan timeout must be a positive number of seconds.";
|
||||||
|
IsError = true;
|
||||||
|
IsSaving = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_configService.SetSyncApiEndpoint(SyncApiEndpoint?.Trim() ?? string.Empty);
|
||||||
|
_configService.SetScanTimeoutSeconds(seconds);
|
||||||
|
_configService.SetAdminCardId(AdminCardId?.Trim() ?? string.Empty);
|
||||||
|
SaveMessage = "Settings saved.";
|
||||||
|
IsError = false;
|
||||||
|
IsSaving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Back()
|
||||||
|
{
|
||||||
|
_navigation.NavigateBackFromSettings(GetDashboardTimeout());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan GetDashboardTimeout()
|
||||||
|
{
|
||||||
|
var seconds = _configService.GetScanTimeoutSeconds();
|
||||||
|
if (seconds <= 0)
|
||||||
|
seconds = 60;
|
||||||
|
return TimeSpan.FromSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
<UserControl x:Class="UtopiaCanteenSystem.Views.AdminLoginView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/>
|
||||||
|
<SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/>
|
||||||
|
<SolidColorBrush x:Key="TitleTextBrush" Color="#2D3748"/>
|
||||||
|
<SolidColorBrush x:Key="ErrorTextBrush" Color="#E53E3E"/>
|
||||||
|
|
||||||
|
<Style x:Key="RoundedButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="Padding" Value="24,12"/>
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="TextInputStyle" TargetType="TextBox">
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="MinHeight" Value="46"/>
|
||||||
|
<Setter Property="Padding" Value="14,0"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#995BA3A0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PasswordInputStyle" TargetType="PasswordBox">
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="MinHeight" Value="46"/>
|
||||||
|
<Setter Property="Padding" Value="14,0"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#995BA3A0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="#F0F2F5">
|
||||||
|
<Viewbox Stretch="Uniform"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="650"
|
||||||
|
MaxHeight="900">
|
||||||
|
<Border HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Padding="40"
|
||||||
|
MinWidth="450"
|
||||||
|
MaxWidth="560"
|
||||||
|
Background="White"
|
||||||
|
CornerRadius="12">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="24"
|
||||||
|
ShadowDepth="0"
|
||||||
|
Opacity="0.08"
|
||||||
|
Color="#000000"/>
|
||||||
|
</Border.Effect>
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Utopia Canteen System"
|
||||||
|
FontSize="32"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource TitleTextBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Admin Login"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Username"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
<TextBox x:Name="UsernameTextBox"
|
||||||
|
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource TextInputStyle}"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Password"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
<PasswordBox x:Name="PasswordBox"
|
||||||
|
Style="{StaticResource PasswordInputStyle}"
|
||||||
|
PasswordChanged="PasswordBox_OnPasswordChanged"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
|
||||||
|
<Button Content="LOGIN"
|
||||||
|
Command="{Binding AdminLoginCommand}"
|
||||||
|
Style="{StaticResource RoundedButtonStyle}"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ErrorMessage}"
|
||||||
|
Foreground="{StaticResource ErrorTextBrush}"
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Viewbox>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal code-behind for PasswordBox + initial focus.
|
||||||
|
/// </summary>
|
||||||
|
public partial class AdminLoginView : UserControl
|
||||||
|
{
|
||||||
|
private AdminLoginViewModel? _vm;
|
||||||
|
|
||||||
|
public AdminLoginView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
// Ensure stale PasswordBox contents never carry across sessions (PasswordBox isn't bindable).
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
if (DataContext is AdminLoginViewModel vm)
|
||||||
|
vm.Password = string.Empty;
|
||||||
|
|
||||||
|
UsernameTextBox.Focus();
|
||||||
|
UsernameTextBox.SelectAll();
|
||||||
|
}, DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_vm != null)
|
||||||
|
_vm.PropertyChanged -= VmOnPropertyChanged;
|
||||||
|
|
||||||
|
_vm = DataContext as AdminLoginViewModel;
|
||||||
|
if (_vm != null)
|
||||||
|
_vm.PropertyChanged += VmOnPropertyChanged;
|
||||||
|
|
||||||
|
// When we navigate back here after logout, the view might be reused; always clear password UI.
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
if (DataContext is AdminLoginViewModel vm)
|
||||||
|
vm.Password = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VmOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!string.Equals(e.PropertyName, nameof(AdminLoginViewModel.Password), StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Keep PasswordBox UI in sync when VM clears password after clicking Login.
|
||||||
|
if (_vm != null && string.IsNullOrEmpty(_vm.Password) && !string.IsNullOrEmpty(PasswordBox.Password))
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PasswordBox_OnPasswordChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AdminLoginViewModel vm)
|
||||||
|
vm.Password = PasswordBox.Password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
<UserControl x:Class="UtopiaCanteenSystem.Views.AdminSettingsAuthView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/>
|
||||||
|
<SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/>
|
||||||
|
<SolidColorBrush x:Key="TitleTextBrush" Color="#2D3748"/>
|
||||||
|
<SolidColorBrush x:Key="ErrorTextBrush" Color="#E53E3E"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0"/>
|
||||||
|
|
||||||
|
<Style x:Key="RoundedButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="Padding" Value="24,12"/>
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="#f4f7f7"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="20,12"/>
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="TextInputStyle" TargetType="TextBox">
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="MinHeight" Value="46"/>
|
||||||
|
<Setter Property="Padding" Value="14,0"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#995BA3A0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PasswordInputStyle" TargetType="PasswordBox">
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="MinHeight" Value="46"/>
|
||||||
|
<Setter Property="Padding" Value="14,0"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#995BA3A0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="#F0F2F5">
|
||||||
|
<Viewbox Stretch="Uniform"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="700"
|
||||||
|
MaxHeight="900">
|
||||||
|
<Border HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Padding="40"
|
||||||
|
MinWidth="520"
|
||||||
|
MaxWidth="620"
|
||||||
|
Background="White"
|
||||||
|
CornerRadius="12">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="24"
|
||||||
|
ShadowDepth="0"
|
||||||
|
Opacity="0.08"
|
||||||
|
Color="#000000"/>
|
||||||
|
</Border.Effect>
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Admin Authentication Required"
|
||||||
|
FontSize="28"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource TitleTextBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Enter admin credentials to open Settings."
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Username"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
<TextBox x:Name="UsernameTextBox"
|
||||||
|
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource TextInputStyle}"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Password"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
<PasswordBox x:Name="PasswordBox"
|
||||||
|
Style="{StaticResource PasswordInputStyle}"
|
||||||
|
PasswordChanged="PasswordBox_OnPasswordChanged"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="16"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Button Grid.Column="0"
|
||||||
|
Content="Cancel"
|
||||||
|
Command="{Binding CancelCommand}"
|
||||||
|
Style="{StaticResource SecondaryButtonStyle}"/>
|
||||||
|
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="Confirm"
|
||||||
|
Command="{Binding ConfirmCommand}"
|
||||||
|
Style="{StaticResource RoundedButtonStyle}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ErrorMessage}"
|
||||||
|
Foreground="{StaticResource ErrorTextBrush}"
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Viewbox>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal code-behind for PasswordBox + initial focus.
|
||||||
|
/// </summary>
|
||||||
|
public partial class AdminSettingsAuthView : UserControl
|
||||||
|
{
|
||||||
|
private AdminSettingsAuthViewModel? _vm;
|
||||||
|
|
||||||
|
public AdminSettingsAuthView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
if (DataContext is AdminSettingsAuthViewModel vm)
|
||||||
|
vm.Password = string.Empty;
|
||||||
|
|
||||||
|
UsernameTextBox.Focus();
|
||||||
|
UsernameTextBox.SelectAll();
|
||||||
|
}, DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_vm != null)
|
||||||
|
_vm.PropertyChanged -= VmOnPropertyChanged;
|
||||||
|
|
||||||
|
_vm = DataContext as AdminSettingsAuthViewModel;
|
||||||
|
if (_vm != null)
|
||||||
|
_vm.PropertyChanged += VmOnPropertyChanged;
|
||||||
|
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
if (DataContext is AdminSettingsAuthViewModel vm)
|
||||||
|
vm.Password = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VmOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!string.Equals(e.PropertyName, nameof(AdminSettingsAuthViewModel.Password), StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_vm != null && string.IsNullOrEmpty(_vm.Password) && !string.IsNullOrEmpty(PasswordBox.Password))
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PasswordBox_OnPasswordChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AdminSettingsAuthViewModel vm)
|
||||||
|
vm.Password = PasswordBox.Password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
<UserControl x:Class="UtopiaCanteenSystem.Views.MainDashboardView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#1a2e35"/>
|
||||||
|
<SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/>
|
||||||
|
|
||||||
|
<Style x:Key="StatusBorderStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#dcfce7"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#86efac"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
<Setter Property="Padding" Value="20"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSuccess}" Value="False">
|
||||||
|
<Setter Property="Background" Value="#fee2e2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#fca5a5"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="StatusTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#22c55e"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSuccess}" Value="False">
|
||||||
|
<Setter Property="Foreground" Value="#ef4444"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="StatusIconStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="✔"/>
|
||||||
|
<Setter Property="Foreground" Value="#22c55e"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSuccess}" Value="False">
|
||||||
|
<Setter Property="Text" Value="✖"/>
|
||||||
|
<Setter Property="Foreground" Value="#ef4444"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="RoundedButtonBase" TargetType="Button">
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="24,12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SecondaryButtonStyle" TargetType="Button" BasedOn="{StaticResource RoundedButtonBase}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#e2e8f0"/>
|
||||||
|
<Setter Property="Foreground" Value="#1a2e35"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SettingsButtonStyle" TargetType="Button" BasedOn="{StaticResource RoundedButtonBase}">
|
||||||
|
<Setter Property="Background" Value="#f4f7f7"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#e2e8f0"/>
|
||||||
|
<Setter Property="Foreground" Value="#1a2e35"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="#F0F2F5">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
Padding="40">
|
||||||
|
<Border HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Width="{Binding RelativeSource={RelativeSource AncestorType=ScrollViewer}, Path=ViewportWidth}"
|
||||||
|
MaxWidth="1400"
|
||||||
|
Padding="40"
|
||||||
|
Background="White"
|
||||||
|
CornerRadius="12">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="24" Opacity="0.08" ShadowDepth="4"/>
|
||||||
|
</Border.Effect>
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<Border Padding="24" BorderThickness="0,0,0,1" BorderBrush="#e2e8f0" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Utopia Canteen System"
|
||||||
|
FontSize="32"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource PrimaryTextBrush}"/>
|
||||||
|
<TextBlock Text="Dashboard"
|
||||||
|
FontSize="18"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
Margin="0,6,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Margin="24,0,24,24">
|
||||||
|
<Border Background="#f4f7f7" CornerRadius="8" Padding="24" Margin="0,0,0,16">
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,10">
|
||||||
|
<Canvas Width="40" Height="40">
|
||||||
|
<Rectangle Width="3" Height="30" Fill="{StaticResource AccentBrush}" Canvas.Left="2" Canvas.Top="5"/>
|
||||||
|
<Rectangle Width="2" Height="30" Fill="{StaticResource AccentBrush}" Canvas.Left="8" Canvas.Top="5"/>
|
||||||
|
<Rectangle Width="5" Height="30" Fill="{StaticResource AccentBrush}" Canvas.Left="13" Canvas.Top="5"/>
|
||||||
|
<Rectangle Width="2" Height="30" Fill="{StaticResource AccentBrush}" Canvas.Left="20" Canvas.Top="5"/>
|
||||||
|
<Rectangle Width="6" Height="30" Fill="{StaticResource AccentBrush}" Canvas.Left="25" Canvas.Top="5"/>
|
||||||
|
<Rectangle Width="3" Height="30" Fill="{StaticResource AccentBrush}" Canvas.Left="33" Canvas.Top="5"/>
|
||||||
|
</Canvas>
|
||||||
|
<TextBlock Text="{Binding ScanCount}"
|
||||||
|
FontSize="56"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource PrimaryTextBrush}"
|
||||||
|
Margin="12,0,0,0"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="Today's Scans"
|
||||||
|
FontSize="18"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border BorderBrush="#e2e8f0" BorderThickness="1" CornerRadius="8" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<Border Padding="16" BorderBrush="#e2e8f0" BorderThickness="0,0,0,1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Width="40" Height="40" Background="#f4f7f7" CornerRadius="6" VerticalAlignment="Center">
|
||||||
|
<Viewbox Margin="6">
|
||||||
|
<Canvas Width="28" Height="28">
|
||||||
|
<Rectangle Width="3" Height="20" Fill="{StaticResource AccentBrush}" Canvas.Left="2" Canvas.Top="4"/>
|
||||||
|
<Rectangle Width="2" Height="20" Fill="{StaticResource AccentBrush}" Canvas.Left="7" Canvas.Top="4"/>
|
||||||
|
<Rectangle Width="4" Height="20" Fill="{StaticResource AccentBrush}" Canvas.Left="11" Canvas.Top="4"/>
|
||||||
|
<Rectangle Width="2" Height="20" Fill="{StaticResource AccentBrush}" Canvas.Left="17" Canvas.Top="4"/>
|
||||||
|
<Rectangle Width="4" Height="20" Fill="{StaticResource AccentBrush}" Canvas.Left="22" Canvas.Top="4"/>
|
||||||
|
</Canvas>
|
||||||
|
</Viewbox>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Grid.Column="1" Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="LAST CARD ID"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding LastCardId}"
|
||||||
|
FontSize="20"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Foreground="{StaticResource PrimaryTextBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="16">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Width="40" Height="40" Background="#f4f7f7" CornerRadius="6" VerticalAlignment="Center">
|
||||||
|
<Viewbox Margin="6">
|
||||||
|
<Canvas Width="28" Height="28">
|
||||||
|
<Ellipse Width="28" Height="28" Stroke="{StaticResource AccentBrush}" StrokeThickness="2"/>
|
||||||
|
<Line X1="14" Y1="14" X2="14" Y2="7" Stroke="{StaticResource AccentBrush}" StrokeThickness="2" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||||
|
<Line X1="14" Y1="14" X2="20" Y2="14" Stroke="{StaticResource AccentBrush}" StrokeThickness="2" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||||
|
</Canvas>
|
||||||
|
</Viewbox>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Grid.Column="1" Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="LAST SCAN TIME"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding LastScanTime}"
|
||||||
|
FontSize="20"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource PrimaryTextBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource StatusBorderStyle}" Margin="0,0,0,16">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border Width="24" Height="24" CornerRadius="12" BorderThickness="2" Margin="0,0,12,0">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="BorderBrush" Value="#22c55e"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSuccess}" Value="False">
|
||||||
|
<Setter Property="BorderBrush" Value="#ef4444"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<TextBlock Style="{StaticResource StatusIconStyle}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding StatusMessage}" Style="{StaticResource StatusTextStyle}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding CurrentTime}"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<WrapPanel HorizontalAlignment="Center">
|
||||||
|
<Button Command="{Binding BackToLoginCommand}" Style="{StaticResource SecondaryButtonStyle}" Margin="0,0,16,0">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="←" FontSize="18" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Back to Scanner" FontSize="18" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding GoToSettingsCommand}"
|
||||||
|
Style="{StaticResource SettingsButtonStyle}"
|
||||||
|
Visibility="Visible">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="⚙" FontSize="18" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Settings" FontSize="18" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main dashboard view: last scanned card ID, date/time, success/error message.
|
||||||
|
/// No logic in code-behind; all binding and commands in ViewModel.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainDashboardView : UserControl
|
||||||
|
{
|
||||||
|
public MainDashboardView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MainDashboardView(ViewModels.MainDashboardViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
<UserControl x:Class="UtopiaCanteenSystem.Views.ScannerView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#5BA3A0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBorderBrush" Color="#995BA3A0"/>
|
||||||
|
<SolidColorBrush x:Key="MutedTextBrush" Color="#718096"/>
|
||||||
|
<SolidColorBrush x:Key="TitleTextBrush" Color="#2D3748"/>
|
||||||
|
<SolidColorBrush x:Key="ErrorTextBrush" Color="#E53E3E"/>
|
||||||
|
<SolidColorBrush x:Key="ConnectedBrush" Color="#38A169"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessTextBrush" Color="#38A169"/>
|
||||||
|
|
||||||
|
<Style x:Key="RoundedButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="Padding" Value="24,12"/>
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="#f4f7f7"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#e2e8f0"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="14,8"/>
|
||||||
|
<Setter Property="MinHeight" Value="40"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="ButtonBorder"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="ButtonBorder" Property="Background" Value="#eef2f2"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="ButtonBorder" Property="Background" Value="#e6eded"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="LogoutLinkButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TitleTextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Opacity" Value="0.75"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="StatusTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ScannerStatus}" Value="Connected">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ConnectedBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding ScannerStatus}" Value="Disconnected">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="StatusIconStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Text" Value=""/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MutedTextBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ScannerStatus}" Value="Connected">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ConnectedBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding ScannerStatus}" Value="Disconnected">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ErrorTextBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="#F0F2F5">
|
||||||
|
<Viewbox Stretch="Uniform"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="600"
|
||||||
|
MaxHeight="800">
|
||||||
|
<Border HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Padding="40"
|
||||||
|
MinWidth="400"
|
||||||
|
MaxWidth="500"
|
||||||
|
Background="White"
|
||||||
|
CornerRadius="12">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="24"
|
||||||
|
ShadowDepth="0"
|
||||||
|
Opacity="0.08"
|
||||||
|
Color="#000000"/>
|
||||||
|
</Border.Effect>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Row 0: main scanner content -->
|
||||||
|
<StackPanel Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Utopia Canteen System"
|
||||||
|
FontSize="32"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource TitleTextBrush}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Scan your card to record"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
Margin="0,0,0,24"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
|
||||||
|
<Border Margin="0,0,0,0"
|
||||||
|
MinHeight="50"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderBrush="{StaticResource AccentBorderBrush}"
|
||||||
|
BorderThickness="2">
|
||||||
|
<Grid>
|
||||||
|
<TextBox x:Name="RfidInputTextBox"
|
||||||
|
Text="{Binding CardIdInput, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
FontSize="18"
|
||||||
|
MinHeight="50"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Focusable="True"
|
||||||
|
KeyDown="RfidInputTextBox_OnKeyDown"
|
||||||
|
LostKeyboardFocus="RfidInputTextBox_OnLostKeyboardFocus"/>
|
||||||
|
<TextBlock Text="Scan Card ID"
|
||||||
|
Foreground="Gray"
|
||||||
|
FontSize="18"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
Visibility="{Binding IsWatermarkVisible, Converter={StaticResource BoolToVisibility}}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button Content="SCAN"
|
||||||
|
Command="{Binding ScanCommand}"
|
||||||
|
MinHeight="50"
|
||||||
|
Margin="0,24,0,0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Style="{StaticResource RoundedButtonStyle}"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,24,0,0">
|
||||||
|
<TextBlock Style="{StaticResource StatusIconStyle}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Text="{Binding ScannerStatusDisplay}"
|
||||||
|
Style="{StaticResource StatusTextStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding CurrentScanTime, StringFormat={}{0:MM/dd/yyyy\, hh:mm:ss tt}}"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedTextBrush}"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding Message}"
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ErrorTextBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSuccess}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SuccessTextBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Row 1: Logout bottom-right -->
|
||||||
|
<Grid Grid.Row="1" Margin="0,24,0,0">
|
||||||
|
<Button Command="{Binding LogoutCommand}"
|
||||||
|
Style="{StaticResource LogoutLinkButtonStyle}"
|
||||||
|
Focusable="False"
|
||||||
|
IsTabStop="False"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="⎋" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Logout" FontSize="14" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Viewbox>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scanner view: minimal code-behind for focus management and Enter-key submission.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ScannerView : UserControl
|
||||||
|
{
|
||||||
|
private bool _isUnloaded;
|
||||||
|
|
||||||
|
public ScannerView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
Unloaded += OnUnloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_isUnloaded = false;
|
||||||
|
FocusRfidInput(selectAll: true, DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_isUnloaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FocusRfidInput(bool selectAll, DispatcherPriority priority)
|
||||||
|
{
|
||||||
|
if (_isUnloaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
if (_isUnloaded || !IsVisible || !IsEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RfidInputTextBox.Focus();
|
||||||
|
Keyboard.Focus(RfidInputTextBox);
|
||||||
|
|
||||||
|
if (selectAll)
|
||||||
|
RfidInputTextBox.SelectAll();
|
||||||
|
}, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RfidInputTextBox_OnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// Keep the view "scan ready" by restoring focus to the RFID TextBox.
|
||||||
|
// Use a low priority so clicks (e.g., Logout) complete first.
|
||||||
|
FocusRfidInput(selectAll: false, DispatcherPriority.ApplicationIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RfidInputTextBox_OnKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key != Key.Enter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (DataContext is ScannerViewModel vm && vm.ScanCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.ScanCommand.Execute(null);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
<UserControl x:Class="UtopiaCanteenSystem.Views.SettingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="AppBackground" Color="#F5F7FA"/>
|
||||||
|
<SolidColorBrush x:Key="CardBackground" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryText" Color="#0A1628"/>
|
||||||
|
<SolidColorBrush x:Key="MutedText" Color="#64748B"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryAccent" Color="#3B82F6"/>
|
||||||
|
<SolidColorBrush x:Key="PrimaryHover" Color="#2563EB"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#16A34A"/>
|
||||||
|
<SolidColorBrush x:Key="ErrorBrush" Color="#EF4444"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="#E2E8F0"/>
|
||||||
|
<SolidColorBrush x:Key="FocusRingBrush" Color="#1F3B82F6"/>
|
||||||
|
|
||||||
|
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#E2E8F0"/>
|
||||||
|
<Setter Property="Foreground" Value="#0A1628"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="Padding" Value="16,0"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="FocusRing"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderThickness="3"
|
||||||
|
BorderBrush="{StaticResource FocusRingBrush}"
|
||||||
|
Opacity="0"/>
|
||||||
|
<Border x:Name="InputBorder"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
Background="{TemplateBinding Background}">
|
||||||
|
<Border.BorderBrush>
|
||||||
|
<SolidColorBrush x:Name="InputBorderBrush" Color="#E2E8F0"/>
|
||||||
|
</Border.BorderBrush>
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Trigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="InputBorderBrush"
|
||||||
|
Storyboard.TargetProperty="Color"
|
||||||
|
To="#7F3B82F6"
|
||||||
|
Duration="0:0:0.2" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
<Trigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="InputBorderBrush"
|
||||||
|
Storyboard.TargetProperty="Color"
|
||||||
|
To="#E2E8F0"
|
||||||
|
Duration="0:0:0.2" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.ExitActions>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="FocusRing" Property="Opacity" Value="1"/>
|
||||||
|
<Trigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="InputBorderBrush"
|
||||||
|
Storyboard.TargetProperty="Color"
|
||||||
|
To="#3B82F6"
|
||||||
|
Duration="0:0:0.2" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="InputBorder" Property="Background" Value="#F8FAFC"/>
|
||||||
|
<Setter Property="Foreground" Value="#94A3B8"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource PrimaryAccent}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="24,14"/>
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="ButtonBorder"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="ButtonBorder" Property="Background" Value="{StaticResource PrimaryHover}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="ButtonBorder" Property="RenderTransform">
|
||||||
|
<Setter.Value>
|
||||||
|
<ScaleTransform ScaleX="0.98" ScaleY="0.98" />
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="OutlineButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource PrimaryText}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="24,14"/>
|
||||||
|
<Setter Property="MinHeight" Value="50"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="ButtonBorder"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="ButtonBorder" Property="Background" Value="#F5F7FA"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="ButtonBorder" Property="RenderTransform">
|
||||||
|
<Setter.Value>
|
||||||
|
<ScaleTransform ScaleX="0.98" ScaleY="0.98" />
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="{StaticResource AppBackground}">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
Padding="40">
|
||||||
|
<StackPanel HorizontalAlignment="Center"
|
||||||
|
Width="{Binding RelativeSource={RelativeSource AncestorType=ScrollViewer}, Path=ViewportWidth}"
|
||||||
|
MaxWidth="1200">
|
||||||
|
<!-- Title section -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,24">
|
||||||
|
<Border Width="48" Height="48" Background="#E7F0FE" CornerRadius="24" Margin="0,0,16,0">
|
||||||
|
<Viewbox Margin="10">
|
||||||
|
<Canvas Width="28" Height="28">
|
||||||
|
<Ellipse Width="28" Height="28" Stroke="{StaticResource PrimaryAccent}" StrokeThickness="2"/>
|
||||||
|
<Line X1="14" Y1="14" X2="14" Y2="7" Stroke="{StaticResource PrimaryAccent}" StrokeThickness="2" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||||
|
<Line X1="14" Y1="14" X2="20" Y2="14" Stroke="{StaticResource PrimaryAccent}" StrokeThickness="2" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||||
|
</Canvas>
|
||||||
|
</Viewbox>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="Settings"
|
||||||
|
FontSize="32"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource PrimaryText}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Card container -->
|
||||||
|
<Border Background="{StaticResource CardBackground}"
|
||||||
|
CornerRadius="12"
|
||||||
|
BorderBrush="{StaticResource BorderBrush}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="24" ShadowDepth="4" Opacity="0.08" Color="#000000" />
|
||||||
|
</Border.Effect>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Card header -->
|
||||||
|
<StackPanel Grid.Row="0" Margin="32,24,32,16">
|
||||||
|
<TextBlock Text="Configuration"
|
||||||
|
FontSize="24"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource PrimaryText}" />
|
||||||
|
<TextBlock Text="Manage scan settings and admin access."
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource MutedText}"
|
||||||
|
Margin="0,8,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Form fields -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="32,0,32,0">
|
||||||
|
<TextBlock Text="Scan Timeout (seconds)" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" />
|
||||||
|
<TextBox Text="{Binding ScanTimeoutSeconds, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,8" />
|
||||||
|
<TextBlock Text="The minimum time between scans to prevent duplicates."
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedText}"
|
||||||
|
Margin="0,0,0,20" />
|
||||||
|
|
||||||
|
<TextBlock Text="Admin Card ID" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" />
|
||||||
|
<TextBox Text="{Binding AdminCardId, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,8" />
|
||||||
|
<TextBlock Text="Card ID with admin access to settings."
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedText}"
|
||||||
|
Margin="0,0,0,20" />
|
||||||
|
|
||||||
|
<TextBlock Text="Sync API Endpoint (UIND)" FontSize="16" Foreground="{StaticResource PrimaryText}" FontWeight="SemiBold" />
|
||||||
|
<TextBox Text="{Binding SyncApiEndpoint, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Style="{StaticResource ModernTextBoxStyle}" Margin="0,8,0,8" />
|
||||||
|
<TextBlock Text="Endpoint used to sync scan records."
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="{StaticResource MutedText}"
|
||||||
|
Margin="0,0,0,20" />
|
||||||
|
|
||||||
|
<!-- Message area -->
|
||||||
|
<Border Margin="0,0,0,20" Padding="16" CornerRadius="8">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#DCFCE7" />
|
||||||
|
<Setter Property="BorderBrush" Value="#86EFAC" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding SaveMessage}" Value="">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SaveMessage}" Value="{x:Null}">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||||
|
<Setter Property="Background" Value="#FEE2E2" />
|
||||||
|
<Setter Property="BorderBrush" Value="#FCA5A5" />
|
||||||
|
</DataTrigger>
|
||||||
|
<Trigger Property="IsVisible" Value="True">
|
||||||
|
<Trigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||||
|
From="0" To="1" Duration="0:0:0.2" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<TextBlock Text="{Binding SaveMessage}"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{StaticResource SuccessBrush}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsError}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Button row -->
|
||||||
|
<Border Grid.Row="2" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0" Padding="32,16">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
|
||||||
|
<Button Content="Cancel"
|
||||||
|
Command="{Binding BackCommand}"
|
||||||
|
Style="{StaticResource OutlineButtonStyle}"
|
||||||
|
MinWidth="120"
|
||||||
|
Margin="0,0,16,0" />
|
||||||
|
<Button Content="Save"
|
||||||
|
Command="{Binding SaveCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
MinWidth="120" />
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using UtopiaCanteenSystem.ViewModels;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Settings view: editable API endpoint, save button, load from config.
|
||||||
|
/// No logic in code-behind; all binding and commands in ViewModel.
|
||||||
|
/// </summary>
|
||||||
|
public partial class SettingsView : UserControl
|
||||||
|
{
|
||||||
|
public SettingsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingsView(SettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,926 @@
|
||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v8.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v8.0": {
|
||||||
|
"UtopiaCanteenSystem/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"CommunityToolkit.Mvvm": "8.2.2",
|
||||||
|
"Microsoft.EntityFrameworkCore": "8.0.11",
|
||||||
|
"Microsoft.EntityFrameworkCore.Sqlite": "8.0.11",
|
||||||
|
"Microsoft.EntityFrameworkCore.Tools": "8.0.11"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"UtopiaCanteenSystem.dll": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CommunityToolkit.Mvvm/8.2.2": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/CommunityToolkit.Mvvm.dll": {
|
||||||
|
"assemblyVersion": "8.2.0.0",
|
||||||
|
"fileVersion": "8.2.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Humanizer.Core/2.14.1": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/Humanizer.dll": {
|
||||||
|
"assemblyVersion": "2.14.0.0",
|
||||||
|
"fileVersion": "2.14.1.48190"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.21.52210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.Common/4.5.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.dll": {
|
||||||
|
"assemblyVersion": "4.5.0.0",
|
||||||
|
"fileVersion": "4.500.23.10905"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "cs"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "de"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "es"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "fr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "it"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "ja"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "ko"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "pl"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "pt-BR"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "ru"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "tr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "zh-Hans"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
|
"locale": "zh-Hant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.CodeAnalysis.Common": "4.5.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
|
||||||
|
"assemblyVersion": "4.5.0.0",
|
||||||
|
"fileVersion": "4.500.23.10905"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "cs"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "de"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "es"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "fr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "it"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "ja"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "ko"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "pl"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "pt-BR"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "ru"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "tr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "zh-Hans"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
|
"locale": "zh-Hant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Humanizer.Core": "2.14.1",
|
||||||
|
"Microsoft.CodeAnalysis.CSharp": "4.5.0",
|
||||||
|
"Microsoft.CodeAnalysis.Common": "4.5.0",
|
||||||
|
"Microsoft.CodeAnalysis.Workspaces.Common": "4.5.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.Workspaces.dll": {
|
||||||
|
"assemblyVersion": "4.5.0.0",
|
||||||
|
"fileVersion": "4.500.23.10905"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "cs"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "de"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "es"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "fr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "it"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "ja"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "ko"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "pl"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "pt-BR"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "ru"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "tr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "zh-Hans"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll": {
|
||||||
|
"locale": "zh-Hant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Humanizer.Core": "2.14.1",
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
||||||
|
"Microsoft.CodeAnalysis.Common": "4.5.0",
|
||||||
|
"System.Composition": "6.0.0",
|
||||||
|
"System.IO.Pipelines": "6.0.3"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.Workspaces.dll": {
|
||||||
|
"assemblyVersion": "4.5.0.0",
|
||||||
|
"fileVersion": "4.500.23.10905"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "cs"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "de"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "es"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "fr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "it"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "ja"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "ko"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "pl"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "pt-BR"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "ru"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "tr"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "zh-Hans"
|
||||||
|
},
|
||||||
|
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll": {
|
||||||
|
"locale": "zh-Hant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Data.Sqlite.Core/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"SQLitePCLRaw.core": "2.1.6"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Data.Sqlite.dll": {
|
||||||
|
"assemblyVersion": "8.0.11.0",
|
||||||
|
"fileVersion": "8.0.1124.52104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.11",
|
||||||
|
"Microsoft.Extensions.Caching.Memory": "8.0.1",
|
||||||
|
"Microsoft.Extensions.Logging": "8.0.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
|
||||||
|
"assemblyVersion": "8.0.11.0",
|
||||||
|
"fileVersion": "8.0.1124.52104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Abstractions/8.0.11": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.11.0",
|
||||||
|
"fileVersion": "8.0.1124.52104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Design/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"Humanizer.Core": "2.14.1",
|
||||||
|
"Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0",
|
||||||
|
"Microsoft.EntityFrameworkCore.Relational": "8.0.11",
|
||||||
|
"Microsoft.Extensions.DependencyModel": "8.0.2",
|
||||||
|
"Mono.TextTemplating": "2.2.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.EntityFrameworkCore.Design.dll": {
|
||||||
|
"assemblyVersion": "8.0.11.0",
|
||||||
|
"fileVersion": "8.0.1124.52104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Relational/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.EntityFrameworkCore": "8.0.11",
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
||||||
|
"assemblyVersion": "8.0.11.0",
|
||||||
|
"fileVersion": "8.0.1124.52104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Sqlite/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.EntityFrameworkCore.Sqlite.Core": "8.0.11",
|
||||||
|
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Sqlite.Core/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Data.Sqlite.Core": "8.0.11",
|
||||||
|
"Microsoft.EntityFrameworkCore.Relational": "8.0.11",
|
||||||
|
"Microsoft.Extensions.DependencyModel": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.EntityFrameworkCore.Sqlite.dll": {
|
||||||
|
"assemblyVersion": "8.0.11.0",
|
||||||
|
"fileVersion": "8.0.1124.52104"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Tools/8.0.11": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.EntityFrameworkCore.Design": "8.0.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Caching.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.23.53103"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Caching.Memory/8.0.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Options": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Caching.Memory.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.23.53103"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection/8.0.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.DependencyInjection.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel/8.0.2": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.2",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging/8.0.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection": "8.0.1",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Options": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Logging.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.1024.46610"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Options/8.0.2": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||||
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Options.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.224.6711"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Primitives/8.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.Primitives.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.23.53103"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Mono.TextTemplating/2.2.1": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.0/Mono.TextTemplating.dll": {
|
||||||
|
"assemblyVersion": "2.2.0.0",
|
||||||
|
"fileVersion": "2.2.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||||
|
"dependencies": {
|
||||||
|
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
|
||||||
|
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
|
||||||
|
"assemblyVersion": "2.1.6.2060",
|
||||||
|
"fileVersion": "2.1.6.2060"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.core/2.1.6": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.0/SQLitePCLRaw.core.dll": {
|
||||||
|
"assemblyVersion": "2.1.6.2060",
|
||||||
|
"fileVersion": "2.1.6.2060"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||||
|
"runtimeTargets": {
|
||||||
|
"runtimes/browser-wasm/nativeassets/net8.0/e_sqlite3.a": {
|
||||||
|
"rid": "browser-wasm",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-arm/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-arm",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-arm64/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-arm64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-armel/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-armel",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-mips64/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-mips64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-musl-arm/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-musl-arm",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-musl-arm64/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-musl-arm64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-musl-x64/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-musl-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-ppc64le/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-ppc64le",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-s390x/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-s390x",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-x64/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/linux-x86/native/libe_sqlite3.so": {
|
||||||
|
"rid": "linux-x86",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib": {
|
||||||
|
"rid": "maccatalyst-arm64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/maccatalyst-x64/native/libe_sqlite3.dylib": {
|
||||||
|
"rid": "maccatalyst-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/osx-arm64/native/libe_sqlite3.dylib": {
|
||||||
|
"rid": "osx-arm64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/osx-x64/native/libe_sqlite3.dylib": {
|
||||||
|
"rid": "osx-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-arm/native/e_sqlite3.dll": {
|
||||||
|
"rid": "win-arm",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-arm64/native/e_sqlite3.dll": {
|
||||||
|
"rid": "win-arm64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-x64/native/e_sqlite3.dll": {
|
||||||
|
"rid": "win-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-x86/native/e_sqlite3.dll": {
|
||||||
|
"rid": "win-x86",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||||
|
"dependencies": {
|
||||||
|
"SQLitePCLRaw.core": "2.1.6"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0-windows7.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
|
||||||
|
"assemblyVersion": "2.1.6.2060",
|
||||||
|
"fileVersion": "2.1.6.2060"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Composition/6.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Composition.AttributedModel": "6.0.0",
|
||||||
|
"System.Composition.Convention": "6.0.0",
|
||||||
|
"System.Composition.Hosting": "6.0.0",
|
||||||
|
"System.Composition.Runtime": "6.0.0",
|
||||||
|
"System.Composition.TypedParts": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Composition.AttributedModel/6.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/System.Composition.AttributedModel.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.21.52210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Composition.Convention/6.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Composition.AttributedModel": "6.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/System.Composition.Convention.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.21.52210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Composition.Hosting/6.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Composition.Runtime": "6.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/System.Composition.Hosting.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.21.52210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Composition.Runtime/6.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/System.Composition.Runtime.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.21.52210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.Composition.TypedParts/6.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"System.Composition.AttributedModel": "6.0.0",
|
||||||
|
"System.Composition.Hosting": "6.0.0",
|
||||||
|
"System.Composition.Runtime": "6.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/System.Composition.TypedParts.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.21.52210"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.IO.Pipelines/6.0.3": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/System.IO.Pipelines.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.522.21309"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"UtopiaCanteenSystem/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"CommunityToolkit.Mvvm/8.2.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-r0g0k9tGYdrnz8R7T3x5UiokDffeevzK/2P/9SBL6fqLgN8B157MIi/bVUWI1KAz6ZorZrK9AdABCWUeXZZsvA==",
|
||||||
|
"path": "communitytoolkit.mvvm/8.2.2",
|
||||||
|
"hashPath": "communitytoolkit.mvvm.8.2.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Humanizer.Core/2.14.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==",
|
||||||
|
"path": "humanizer.core/2.14.1",
|
||||||
|
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
|
||||||
|
"path": "microsoft.bcl.asyncinterfaces/6.0.0",
|
||||||
|
"hashPath": "microsoft.bcl.asyncinterfaces.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.Common/4.5.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==",
|
||||||
|
"path": "microsoft.codeanalysis.common/4.5.0",
|
||||||
|
"hashPath": "microsoft.codeanalysis.common.4.5.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.CSharp/4.5.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==",
|
||||||
|
"path": "microsoft.codeanalysis.csharp/4.5.0",
|
||||||
|
"hashPath": "microsoft.codeanalysis.csharp.4.5.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.CSharp.Workspaces/4.5.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==",
|
||||||
|
"path": "microsoft.codeanalysis.csharp.workspaces/4.5.0",
|
||||||
|
"hashPath": "microsoft.codeanalysis.csharp.workspaces.4.5.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.Workspaces.Common/4.5.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==",
|
||||||
|
"path": "microsoft.codeanalysis.workspaces.common/4.5.0",
|
||||||
|
"hashPath": "microsoft.codeanalysis.workspaces.common.4.5.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Data.Sqlite.Core/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-PrDkI9SeU/MEP/IHriczeYmRVbzEcfp66UlZRjL5ikHIJGIYOrby55GoehLCJzJiTwJ+rGkjSRctZnWgfC95fg==",
|
||||||
|
"path": "microsoft.data.sqlite.core/8.0.11",
|
||||||
|
"hashPath": "microsoft.data.sqlite.core.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-stbjWBTtpQ1HtqXMFyKnXFTr76PvaOHI2b2h85JqBi3eZr00nspvR/a90Zwh8CQ4rVawqLiTG0+0yZQWaav+sQ==",
|
||||||
|
"path": "microsoft.entityframeworkcore/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Abstractions/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-++zY0Ea724ku1jptWJmF7jm3I4IXTexfT4qi1ETcSFFF7qj+qm6rRgN7mTuKkwIETuXk0ikfzudryRjUGrrNKQ==",
|
||||||
|
"path": "microsoft.entityframeworkcore.abstractions/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.abstractions.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Design/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-KxOvpbaKiUmbLvenr0T/4F1Vdm0Sq+iajLbesQK7/WKB/Dx+FQHCZ0f5jCXrVWK2QKF9eHzQ5JPA1L6hcb25FQ==",
|
||||||
|
"path": "microsoft.entityframeworkcore.design/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.design.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Relational/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-3TuuW3i5I4Ro0yoaHmi2MqEDGObOVuhLaMEnd/heaLB1fcvm4fu4PevmC4BOWnI0vo176AIlV5o4rEQciLoohw==",
|
||||||
|
"path": "microsoft.entityframeworkcore.relational/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.relational.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Sqlite/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-HJN+xx8lomTIq7SpshnUzHt7uo1/AOvnPWjXsOzyCsoYMEpfRKjxsJobcHu8Qpvd2mwzZB/mzjPUE8XeuGiCGA==",
|
||||||
|
"path": "microsoft.entityframeworkcore.sqlite/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.sqlite.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Sqlite.Core/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-wvC/xpis//IG9qvfMbMFMjhrM+P7choZ23CHBRfQyfmIkOVZLBtzM6nestbDdAv3eGnJym1/m0o0sc7YXlL0yg==",
|
||||||
|
"path": "microsoft.entityframeworkcore.sqlite.core/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.sqlite.core.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.EntityFrameworkCore.Tools/8.0.11": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-9iUOj0npm2FxOkGIE3ktv0N0YU1oEhaMTJoDYuKS8dGNkWo1CPm7RjsoJABesKFk1lkCIfTE5SHXb45GIMjDnQ==",
|
||||||
|
"path": "microsoft.entityframeworkcore.tools/8.0.11",
|
||||||
|
"hashPath": "microsoft.entityframeworkcore.tools.8.0.11.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
|
||||||
|
"path": "microsoft.extensions.caching.abstractions/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.caching.abstractions.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Caching.Memory/8.0.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-HFDnhYLccngrzyGgHkjEDU5FMLn4MpOsr5ElgsBMC4yx6lJh4jeWO7fHS8+TXPq+dgxCmUa/Trl8svObmwW4QA==",
|
||||||
|
"path": "microsoft.extensions.caching.memory/8.0.1",
|
||||||
|
"hashPath": "microsoft.extensions.caching.memory.8.0.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
||||||
|
"path": "microsoft.extensions.configuration.abstractions/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.configuration.abstractions.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection/8.0.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
|
||||||
|
"path": "microsoft.extensions.dependencyinjection/8.0.1",
|
||||||
|
"hashPath": "microsoft.extensions.dependencyinjection.8.0.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==",
|
||||||
|
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.2",
|
||||||
|
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel/8.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==",
|
||||||
|
"path": "microsoft.extensions.dependencymodel/8.0.2",
|
||||||
|
"hashPath": "microsoft.extensions.dependencymodel.8.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging/8.0.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
|
||||||
|
"path": "microsoft.extensions.logging/8.0.1",
|
||||||
|
"hashPath": "microsoft.extensions.logging.8.0.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
|
||||||
|
"path": "microsoft.extensions.logging.abstractions/8.0.2",
|
||||||
|
"hashPath": "microsoft.extensions.logging.abstractions.8.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Options/8.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
|
||||||
|
"path": "microsoft.extensions.options/8.0.2",
|
||||||
|
"hashPath": "microsoft.extensions.options.8.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Primitives/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
|
||||||
|
"path": "microsoft.extensions.primitives/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Mono.TextTemplating/2.2.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==",
|
||||||
|
"path": "mono.texttemplating/2.2.1",
|
||||||
|
"hashPath": "mono.texttemplating.2.2.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
|
||||||
|
"path": "sqlitepclraw.bundle_e_sqlite3/2.1.6",
|
||||||
|
"hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.core/2.1.6": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
|
||||||
|
"path": "sqlitepclraw.core/2.1.6",
|
||||||
|
"hashPath": "sqlitepclraw.core.2.1.6.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==",
|
||||||
|
"path": "sqlitepclraw.lib.e_sqlite3/2.1.6",
|
||||||
|
"hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.6.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
|
||||||
|
"path": "sqlitepclraw.provider.e_sqlite3/2.1.6",
|
||||||
|
"hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.6.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Composition/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==",
|
||||||
|
"path": "system.composition/6.0.0",
|
||||||
|
"hashPath": "system.composition.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Composition.AttributedModel/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==",
|
||||||
|
"path": "system.composition.attributedmodel/6.0.0",
|
||||||
|
"hashPath": "system.composition.attributedmodel.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Composition.Convention/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==",
|
||||||
|
"path": "system.composition.convention/6.0.0",
|
||||||
|
"hashPath": "system.composition.convention.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Composition.Hosting/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==",
|
||||||
|
"path": "system.composition.hosting/6.0.0",
|
||||||
|
"hashPath": "system.composition.hosting.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Composition.Runtime/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==",
|
||||||
|
"path": "system.composition.runtime/6.0.0",
|
||||||
|
"hashPath": "system.composition.runtime.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.Composition.TypedParts/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==",
|
||||||
|
"path": "system.composition.typedparts/6.0.0",
|
||||||
|
"hashPath": "system.composition.typedparts.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.IO.Pipelines/6.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==",
|
||||||
|
"path": "system.io.pipelines/6.0.3",
|
||||||
|
"hashPath": "system.io.pipelines.6.0.3.nupkg.sha512"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.WindowsDesktop.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||||
|
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"SyncApiEndpoint": "https://api.example.com/uind/sync",
|
||||||
|
"ScannerConnected": false,
|
||||||
|
"ScanTimeoutSeconds": 3,
|
||||||
|
"AdminCardId": "ADMIN"
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue