diff --git a/.gitignore b/.gitignore
index 25496a5..551f5be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,9 @@ obj/
# Logs
*.log
+# Local config (contains secrets)
+appsettings.json
+
# SQLite database files (local runtime data)
*.db
*.db-shm
diff --git a/App.xaml.cs b/App.xaml.cs
index 67bf3d6..a5754e4 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -44,7 +44,7 @@ public partial class App : Application
() => new ScannerDashboardViewModel(rfidService, navigationService, session, configService),
() => new MainDashboardViewModel(navigationService, rfidService, configService, session),
() => new AdminSettingsAuthViewModel(authService, session, navigationService, configService),
- () => new SettingsViewModel(configService, navigationService, adminAuditService));
+ () => new SettingsViewModel(configService, navigationService, adminAuditService, syncService));
var mainViewModel = new MainViewModel(navigationService);
diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs
index 5458681..cda21f3 100644
--- a/Services/ConfigService.cs
+++ b/Services/ConfigService.cs
@@ -174,7 +174,11 @@ public class ConfigService : IConfigService
SaveConfig();
}
- public string GetMySqlConnectionString() => _config.MySqlConnectionString ?? string.Empty;
+ ///
+ /// Returns MySQL connection string for sync. Set via appsettings.json (copy from appsettings.example.json).
+ /// No default credentials; returns empty if not configured.
+ ///
+ public string GetMySqlConnectionString() => (_config.MySqlConnectionString ?? string.Empty).Trim();
public void SetMySqlConnectionString(string connectionString)
{
@@ -238,7 +242,7 @@ public class ConfigService : IConfigService
public string SavedAdminUsername { get; set; } = string.Empty;
public string SavedAdminPasswordProtected { get; set; } = string.Empty;
- // MySQL connection string for direct DB sync (local testing).
+ // MySQL connection string for sync. Set in appsettings.json (see appsettings.example.json). No default.
public string MySqlConnectionString { get; set; } = string.Empty;
}
}
diff --git a/Services/SyncService.cs b/Services/SyncService.cs
index 5c398dd..d515b6e 100644
--- a/Services/SyncService.cs
+++ b/Services/SyncService.cs
@@ -23,9 +23,11 @@ public class SyncService : ISyncService
public async Task SyncNowAsync(CancellationToken cancellationToken = default)
{
var connectionString = _configService.GetMySqlConnectionString();
- // TODO: For local testing only. Set MySqlConnectionString in appsettings.json or via config to avoid hardcoding.
if (string.IsNullOrWhiteSpace(connectionString))
- connectionString = "Server=localhost;Database=canteen_prod;User=root;Password=Root@12345_;Port=3306;";
+ {
+ System.Diagnostics.Debug.WriteLine("MySQL connection string not configured; skipping sync.");
+ return;
+ }
List toSync;
using (var db = _dbFactory.CreateDbContext())
@@ -37,6 +39,9 @@ public class SyncService : ISyncService
.ConfigureAwait(false);
}
+ // Always run day-end cleanup (remove synced rows from previous days), even when there's nothing to sync.
+ await CleanupOldSyncedRowsAsync(cancellationToken).ConfigureAwait(false);
+
if (toSync.Count == 0)
return;
@@ -52,9 +57,10 @@ public class SyncService : ISyncService
{
// Use INSERT IGNORE to handle duplicates (unique key on DeviceId, DeviceLocalRowId)
// This ensures no duplicates even if sync runs multiple times
+ // Column names match production: hrms.lunch_order_transactions (snake_case)
var insertSql = @"
INSERT IGNORE INTO lunch_order_transactions
- (DeviceLocalRowId, ScanTimeUtc, SiteId, DeviceId, CardId, IpAddress, ReceivedAtUtc)
+ (device_local_row_id, scan_time_utc, site_id, device_id, card_id, ip_address, received_at_utc)
VALUES
(@DeviceLocalRowId, @ScanTimeUtc, @SiteId, @DeviceId, @CardId, @IpAddress, UTC_TIMESTAMP(3))";
diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs
index 99c0b8b..df728c7 100644
--- a/ViewModels/SettingsViewModel.cs
+++ b/ViewModels/SettingsViewModel.cs
@@ -12,6 +12,7 @@ public partial class SettingsViewModel : ObservableObject
private readonly IConfigService _configService;
private readonly INavigationService _navigation;
private readonly IAdminAuditService _adminAudit;
+ private readonly ISyncService _syncService;
[ObservableProperty]
private string _syncApiEndpoint = string.Empty;
@@ -44,11 +45,16 @@ public partial class SettingsViewModel : ObservableObject
OnPropertyChanged(nameof(CanPostNow));
}
- public SettingsViewModel(IConfigService configService, INavigationService navigation, IAdminAuditService adminAudit)
+ public SettingsViewModel(
+ IConfigService configService,
+ INavigationService navigation,
+ IAdminAuditService adminAudit,
+ ISyncService syncService)
{
_configService = configService;
_navigation = navigation;
_adminAudit = adminAudit;
+ _syncService = syncService;
LoadFromConfig();
}
@@ -92,6 +98,41 @@ public partial class SettingsViewModel : ObservableObject
_navigation.NavigateBackFromSettings(GetDashboardTimeout());
}
+ [RelayCommand]
+ private async Task PostDataNow()
+ {
+ if (IsPosting)
+ return;
+
+ SaveMessage = string.Empty;
+ IsError = false;
+ IsPosting = true;
+
+ try
+ {
+ // If not configured, do not attempt sync.
+ if (string.IsNullOrWhiteSpace(_configService.GetMySqlConnectionString()))
+ {
+ SaveMessage = "MySQL connection string not configured.";
+ IsError = true;
+ return;
+ }
+
+ await _syncService.SyncNowAsync().ConfigureAwait(false);
+ SaveMessage = "Posted data to production.";
+ IsError = false;
+ }
+ catch
+ {
+ SaveMessage = "Failed to post data to production.";
+ IsError = true;
+ }
+ finally
+ {
+ IsPosting = false;
+ }
+ }
+
private TimeSpan GetDashboardTimeout()
{
var seconds = _configService.GetScanTimeoutSeconds();
diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml
index 1c54146..8be04b4 100644
--- a/Views/SettingsView.xaml
+++ b/Views/SettingsView.xaml
@@ -251,7 +251,7 @@
FontSize="14"
Foreground="{StaticResource MutedText}"
Margin="0,0,0,20" />-->
-
@@ -263,9 +263,12 @@
-
+
-
+
@@ -361,7 +364,7 @@
-