2026-09-16 20:09:19 +00:00
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Windows;
|
2026-09-30 18:50:04 +00:00
|
|
|
using System.Windows.Controls;
|
|
|
|
|
using System.Windows.Media;
|
2026-09-16 20:09:19 +00:00
|
|
|
using InvoiceParser.Src;
|
|
|
|
|
|
|
|
|
|
namespace InvoiceParser;
|
|
|
|
|
|
|
|
|
|
public partial class App : Application
|
|
|
|
|
{
|
|
|
|
|
private WebHost? _host;
|
|
|
|
|
private Mutex? _singleInstance;
|
|
|
|
|
|
|
|
|
|
protected override async void OnStartup(StartupEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
base.OnStartup(e);
|
|
|
|
|
|
|
|
|
|
// Nothing else catches these, and a silent crash on an unattended
|
|
|
|
|
// machine is exactly what the log exists to explain.
|
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
|
|
|
|
Log.Error("crash", "unhandled exception", args.ExceptionObject as Exception);
|
|
|
|
|
DispatcherUnhandledException += (_, args) =>
|
|
|
|
|
Log.Error("crash", "unhandled UI exception", args.Exception);
|
|
|
|
|
TaskScheduler.UnobservedTaskException += (_, args) =>
|
|
|
|
|
{
|
|
|
|
|
Log.Error("crash", "unobserved task exception", args.Exception);
|
|
|
|
|
args.SetObserved();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_singleInstance = new Mutex(true, "InvoiceParserApp_SingleInstance", out bool isNew);
|
|
|
|
|
if (!isNew)
|
|
|
|
|
{
|
|
|
|
|
// Bring the running instance forward instead of just saying it exists.
|
|
|
|
|
FocusRunningInstance();
|
|
|
|
|
Shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var config = AppConfig.Load(AppContext.BaseDirectory);
|
2026-09-30 18:50:04 +00:00
|
|
|
|
|
|
|
|
// The login dialog would otherwise become MainWindow and end the app when it closes.
|
|
|
|
|
ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
|
|
|
|
var emp = PromptLogin(config);
|
|
|
|
|
if (emp == null)
|
|
|
|
|
{
|
|
|
|
|
Log.Info("login", "login cancelled");
|
|
|
|
|
Shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
config.ReceiverEmpNo = emp.Value.EmpNo;
|
|
|
|
|
config.ReceiverFirstName = emp.Value.FirstName;
|
|
|
|
|
Log.Info("login", "logged in as emp_no " + emp.Value.EmpNo);
|
|
|
|
|
ShutdownMode = ShutdownMode.OnMainWindowClose;
|
|
|
|
|
|
2026-09-16 20:09:19 +00:00
|
|
|
_host = new WebHost(config);
|
|
|
|
|
await _host.StartAsync();
|
|
|
|
|
|
|
|
|
|
var win = new MainWindow(_host.BaseUrl);
|
|
|
|
|
MainWindow = win;
|
|
|
|
|
win.Show();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Error("startup", "startup failed", ex);
|
|
|
|
|
MessageBox.Show("Startup failed: " + ex.Message, "Invoice Parser",
|
|
|
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
|
|
|
Shutdown();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 18:50:04 +00:00
|
|
|
// Returns the employee whose AdminPassword was entered, or null if cancelled.
|
|
|
|
|
private static (int EmpNo, string FirstName)? PromptLogin(AppConfig cfg)
|
|
|
|
|
{
|
|
|
|
|
(int EmpNo, string FirstName)? empNo = null;
|
|
|
|
|
var pw = new PasswordBox { FontSize = 16, Margin = new Thickness(0, 6, 0, 0) };
|
|
|
|
|
var error = new TextBlock { Foreground = Brushes.Firebrick, Margin = new Thickness(0, 8, 0, 0) };
|
|
|
|
|
var ok = new Button { Content = "Log in", IsDefault = true, Width = 90, Margin = new Thickness(0, 12, 8, 0) };
|
|
|
|
|
var cancel = new Button { Content = "Cancel", IsCancel = true, Width = 90, Margin = new Thickness(0, 12, 0, 0) };
|
|
|
|
|
var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
|
|
|
|
|
buttons.Children.Add(ok);
|
|
|
|
|
buttons.Children.Add(cancel);
|
|
|
|
|
var panel = new StackPanel { Margin = new Thickness(20), MinWidth = 300 };
|
|
|
|
|
panel.Children.Add(new TextBlock { Text = "Admin password" });
|
|
|
|
|
panel.Children.Add(pw);
|
|
|
|
|
panel.Children.Add(error);
|
|
|
|
|
panel.Children.Add(buttons);
|
|
|
|
|
|
|
|
|
|
var win = new Window
|
|
|
|
|
{
|
|
|
|
|
Title = "Invoice Parser - Log in",
|
|
|
|
|
Content = panel,
|
|
|
|
|
SizeToContent = SizeToContent.WidthAndHeight,
|
|
|
|
|
ResizeMode = ResizeMode.NoResize,
|
|
|
|
|
WindowStartupLocation = WindowStartupLocation.CenterScreen
|
|
|
|
|
};
|
|
|
|
|
win.Loaded += (_, _) => pw.Focus();
|
|
|
|
|
ok.Click += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
error.Text = "";
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
empNo = LookupEmployee(cfg, pw.Password);
|
|
|
|
|
if (empNo != null) { win.DialogResult = true; return; }
|
|
|
|
|
error.Text = "Invalid password";
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Error("login", "employee lookup failed", ex);
|
|
|
|
|
error.Text = "Database error: " + ex.Message;
|
|
|
|
|
}
|
|
|
|
|
pw.Clear();
|
|
|
|
|
pw.Focus();
|
|
|
|
|
};
|
|
|
|
|
return win.ShowDialog() == true ? empNo : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static (int EmpNo, string FirstName)? LookupEmployee(AppConfig cfg, string password)
|
|
|
|
|
{
|
|
|
|
|
if (!int.TryParse(password.Trim(), out var pw)) return null;
|
|
|
|
|
using var conn = new Db(cfg).Open();
|
|
|
|
|
using var cmd = conn.CreateCommand();
|
|
|
|
|
cmd.CommandText =
|
|
|
|
|
"SELECT TOP 1 emp_no, FirstName FROM posbdat.dbo.Employees " +
|
|
|
|
|
"WHERE AdminPassword = @pw AND ISNULL(EmpActive, 0) = 1";
|
|
|
|
|
cmd.Parameters.AddWithValue("@pw", pw);
|
|
|
|
|
using var r = cmd.ExecuteReader();
|
|
|
|
|
if (!r.Read()) return null;
|
|
|
|
|
return (Convert.ToInt32(r.GetValue(0)), (Db.GetStr(r, "FirstName") ?? "").Trim());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 20:09:19 +00:00
|
|
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
|
|
|
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
|
|
|
|
|
|
|
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
|
|
|
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
|
|
|
|
|
|
|
|
private static void FocusRunningInstance()
|
|
|
|
|
{
|
|
|
|
|
const int SW_RESTORE = 9;
|
|
|
|
|
var me = Process.GetCurrentProcess();
|
|
|
|
|
foreach (var p in Process.GetProcessesByName(me.ProcessName))
|
|
|
|
|
{
|
|
|
|
|
if (p.Id == me.Id || p.MainWindowHandle == IntPtr.Zero) continue;
|
|
|
|
|
ShowWindow(p.MainWindowHandle, SW_RESTORE);
|
|
|
|
|
SetForegroundWindow(p.MainWindowHandle);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
MessageBox.Show("Invoice Parser is already running.", "Invoice Parser",
|
|
|
|
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnExit(ExitEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
// Stop the host off the UI thread: awaiting it here can deadlock on the
|
|
|
|
|
// WPF SynchronizationContext, leaving a windowless process holding the
|
|
|
|
|
// single-instance mutex.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var host = _host;
|
|
|
|
|
if (host != null)
|
|
|
|
|
Task.Run(() => host.StopAsync()).Wait(TimeSpan.FromSeconds(5));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) { Log.Error("shutdown", "web host stop failed", ex); }
|
|
|
|
|
Log.Info("shutdown", "exiting with code " + e.ApplicationExitCode);
|
|
|
|
|
_singleInstance?.Dispose();
|
|
|
|
|
base.OnExit(e);
|
|
|
|
|
// Hard terminate. Environment.Exit runs finalizers and COM cleanup, which
|
|
|
|
|
// hangs on WebView2's browser processes and leaves a windowless process
|
|
|
|
|
// that holds the single-instance mutex and locks the exe against rebuilds.
|
|
|
|
|
// Safe here: every state.json write is synchronous and already flushed,
|
|
|
|
|
// and a parse abandoned mid-call is reset from "processing" to "queued"
|
|
|
|
|
// by ParseWorker on the next start.
|
|
|
|
|
Process.GetCurrentProcess().Kill();
|
|
|
|
|
}
|
|
|
|
|
}
|