Removed large files FiX
parent
de280dab2d
commit
6e2b838539
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": ".NET Core Launch (console)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/bin/Debug/net9.0-windows10.0.19041.0/win-x64/FrymasterBadgeApp.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/FrymasterBadgeApp.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/FrymasterBadgeApp.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/FrymasterBadgeApp.csproj"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
8
App.xaml
8
App.xaml
|
|
@ -1,13 +1,15 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<Application
|
<Application
|
||||||
|
x:Class="FrymasterBadgeApp.App"
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="FrymasterBadgeApp.App"
|
xmlns:local="clr-namespace:FrymasterBadgeApp">
|
||||||
>
|
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
||||||
|
|
||||||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
243
App.xaml.cs
243
App.xaml.cs
|
|
@ -1,176 +1,99 @@
|
||||||
using System.Diagnostics;
|
namespace FrymasterBadgeApp;
|
||||||
using FrymasterBadgeApp.Services;
|
|
||||||
using Microsoft.Maui.Controls;
|
|
||||||
using Microsoft.Maui.Controls.PlatformConfiguration.WindowsSpecific;
|
|
||||||
|
|
||||||
namespace FrymasterBadgeApp;
|
public partial class App : Application
|
||||||
|
|
||||||
public partial class App : Microsoft.Maui.Controls.Application
|
|
||||||
{
|
{
|
||||||
private readonly SqlService _db;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly PrinterService _printerService;
|
private const string ThemePrefKey = "AppTheme"; // Must match SettingsPage
|
||||||
private Microsoft.Maui.Controls.TabbedPage _rootTabbedPage;
|
|
||||||
|
|
||||||
public App(SqlService db, PrinterService printerService)
|
public App(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
AppLogger.Info("App: Constructor - Internal setup beginning");
|
||||||
_db = db;
|
|
||||||
_printerService = printerService;
|
|
||||||
|
|
||||||
Debug.WriteLine("=== APP STARTUP ===");
|
|
||||||
|
|
||||||
_rootTabbedPage = new Microsoft.Maui.Controls.TabbedPage
|
|
||||||
{
|
|
||||||
Title = "Frymaster Badge System",
|
|
||||||
BarBackgroundColor = Colors.SlateGray,
|
|
||||||
BarTextColor = Colors.White,
|
|
||||||
};
|
|
||||||
|
|
||||||
_rootTabbedPage
|
|
||||||
.On<Microsoft.Maui.Controls.PlatformConfiguration.Windows>()
|
|
||||||
.SetHeaderIconsEnabled(false);
|
|
||||||
|
|
||||||
var loadingPage = new Microsoft.Maui.Controls.ContentPage
|
|
||||||
{
|
|
||||||
Title = "Loading",
|
|
||||||
BackgroundColor = Colors.DarkSlateBlue,
|
|
||||||
Content = new StackLayout
|
|
||||||
{
|
|
||||||
VerticalOptions = LayoutOptions.Center,
|
|
||||||
HorizontalOptions = LayoutOptions.Center,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new ActivityIndicator
|
|
||||||
{
|
|
||||||
IsRunning = true,
|
|
||||||
Color = Colors.White,
|
|
||||||
Scale = 2,
|
|
||||||
},
|
|
||||||
new Microsoft.Maui.Controls.Label
|
|
||||||
{
|
|
||||||
Text = "Loading companies...",
|
|
||||||
TextColor = Colors.White,
|
|
||||||
FontSize = 18,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
_rootTabbedPage.Children.Add(loadingPage);
|
|
||||||
MainPage = _rootTabbedPage;
|
|
||||||
|
|
||||||
Debug.WriteLine("App: Resources loaded, loading page added, TabbedPage set as MainPage");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async void OnStart()
|
|
||||||
{
|
|
||||||
base.OnStart();
|
|
||||||
Debug.WriteLine("App: OnStart - calling LoadTabsAsync");
|
|
||||||
await LoadTabsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadTabsAsync()
|
|
||||||
{
|
|
||||||
Debug.WriteLine("LoadTabsAsync: ENTERED");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Debug.WriteLine("LoadTabsAsync: Querying database for companies...");
|
InitializeComponent();
|
||||||
var companies = await Task.Run(() => _db.Query("SELECT * FROM dbo.Companies", null));
|
ApplySavedTheme(); // Set the theme before resolving Shell
|
||||||
|
AppLogger.Info("App: InitializeComponent and Theme application successful");
|
||||||
Debug.WriteLine($"LoadTabsAsync: Query returned {companies?.Count ?? 0} companies");
|
|
||||||
|
|
||||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
|
||||||
{
|
|
||||||
Debug.WriteLine("LoadTabsAsync: On main thread - delay");
|
|
||||||
await Task.Delay(200);
|
|
||||||
|
|
||||||
Debug.WriteLine(
|
|
||||||
$"LoadTabsAsync: Current children count before clear: {_rootTabbedPage.Children.Count}"
|
|
||||||
);
|
|
||||||
_rootTabbedPage.Children.Clear();
|
|
||||||
Debug.WriteLine("LoadTabsAsync: Children cleared");
|
|
||||||
|
|
||||||
if (companies == null || !companies.Any())
|
|
||||||
{
|
|
||||||
Debug.WriteLine("LoadTabsAsync: No companies - adding setup page");
|
|
||||||
|
|
||||||
var setupPage = new Microsoft.Maui.Controls.ContentPage
|
|
||||||
{
|
|
||||||
Title = "Setup",
|
|
||||||
BackgroundColor = Colors.Purple,
|
|
||||||
Content = new StackLayout
|
|
||||||
{
|
|
||||||
VerticalOptions = LayoutOptions.Center,
|
|
||||||
HorizontalOptions = LayoutOptions.Center,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Microsoft.Maui.Controls.Label
|
|
||||||
{
|
|
||||||
Text = "No companies found - click to manage",
|
|
||||||
TextColor = Colors.White,
|
|
||||||
FontSize = 24,
|
|
||||||
},
|
|
||||||
new Microsoft.Maui.Controls.Button
|
|
||||||
{
|
|
||||||
Text = "Manage Companies",
|
|
||||||
Command = new Command(async () =>
|
|
||||||
await Microsoft.Maui.Controls.Application.Current.MainPage.Navigation.PushAsync(
|
|
||||||
new CompanyPage(_db)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
_rootTabbedPage.Children.Add(setupPage);
|
|
||||||
Debug.WriteLine("LoadTabsAsync: Setup page added");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.WriteLine($"LoadTabsAsync: Adding {companies.Count} real tabs");
|
|
||||||
|
|
||||||
int added = 0;
|
|
||||||
foreach (var company in companies)
|
|
||||||
{
|
|
||||||
string companyName = company.GetValueOrDefault("Name")?.ToString() ?? "Unknown";
|
|
||||||
Debug.WriteLine($"LoadTabsAsync: Processing company '{companyName}'");
|
|
||||||
|
|
||||||
var employeePage = new EmployeePage(_db, _printerService, company);
|
|
||||||
employeePage.Title = companyName;
|
|
||||||
employeePage.BackgroundColor = Colors.LightBlue; // Temporary for visibility
|
|
||||||
|
|
||||||
employeePage.ToolbarItems.Add(
|
|
||||||
new ToolbarItem
|
|
||||||
{
|
|
||||||
Text = "Manage",
|
|
||||||
Command = new Command(async () =>
|
|
||||||
await employeePage.Navigation.PushAsync(new CompanyPage(_db))
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
_rootTabbedPage.Children.Add(employeePage);
|
|
||||||
added++;
|
|
||||||
Debug.WriteLine(
|
|
||||||
$"LoadTabsAsync: Added tab '{companyName}' ({added}/{companies.Count})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.WriteLine($"LoadTabsAsync: Finished - {added} tabs added");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"LoadTabsAsync: EXCEPTION - {ex.Message}");
|
AppLogger.Error("App: FATAL XAML ERROR", ex);
|
||||||
Debug.WriteLine(ex.StackTrace);
|
throw;
|
||||||
|
}
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the saved theme preference to the app.
|
||||||
|
/// If the preference could not be applied, a warning is logged.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplySavedTheme()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pref = Microsoft.Maui.Storage.Preferences.Default.Get(ThemePrefKey, "System");
|
||||||
|
AppLogger.Info($"App: Applying saved theme preference: {pref}");
|
||||||
|
|
||||||
|
Application.Current.UserAppTheme = pref switch
|
||||||
|
{
|
||||||
|
"Light" => AppTheme.Light,
|
||||||
|
"Dark" => AppTheme.Dark,
|
||||||
|
_ => AppTheme.Unspecified
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn($"App: Failed to apply theme preference: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the required AppShell service and
|
||||||
|
/// opens a new window with the specified content.
|
||||||
|
/// </summary>
|
||||||
protected override Window CreateWindow(IActivationState? activationState)
|
protected override Window CreateWindow(IActivationState? activationState)
|
||||||
{
|
{
|
||||||
Debug.WriteLine("CreateWindow called");
|
AppLogger.Info("App: CreateWindow entered");
|
||||||
return new Window(MainPage!);
|
try
|
||||||
|
{
|
||||||
|
var shell = _serviceProvider.GetRequiredService<AppShell>();
|
||||||
|
AppLogger.Info("App: AppShell resolved successfully");
|
||||||
|
return new Window(shell);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error("CRITICAL STARTUP ERROR: Dependency Resolution Failed", ex);
|
||||||
|
|
||||||
|
return new Window(
|
||||||
|
new ContentPage
|
||||||
|
{
|
||||||
|
Content = new VerticalStackLayout
|
||||||
|
{
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
Padding = 20,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = "Startup Failed",
|
||||||
|
FontAttributes = FontAttributes.Bold,
|
||||||
|
HorizontalTextAlignment = TextAlignment.Center,
|
||||||
|
FontSize = 20
|
||||||
|
},
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = ex.Message,
|
||||||
|
HorizontalTextAlignment = TextAlignment.Center,
|
||||||
|
TextColor = Colors.Red,
|
||||||
|
},
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = "Check log for details.",
|
||||||
|
HorizontalTextAlignment = TextAlignment.Center,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
# App Class Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `App` class is the main entry point for the **FrymasterBadgeApp** .NET MAUI application. It inherits from `Microsoft.Maui.Controls.Application` and is responsible for:
|
||||||
|
|
||||||
|
- Initializing the application components
|
||||||
|
- Applying the user's saved theme preference
|
||||||
|
- Setting up dependency injection
|
||||||
|
- Creating the main application window with robust error handling
|
||||||
|
|
||||||
|
## Namespace
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace FrymasterBadgeApp;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class Definition
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class App : Application
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|------------------|-------------------|-----------|
|
||||||
|
| `_serviceProvider` | `IServiceProvider` | Holds the dependency injection container used to resolve services like `AppShell`. |
|
||||||
|
| `ThemePrefKey` | `const string` | Constant key used to store and retrieve the app theme preference. Must match the key used in `SettingsPage`. |
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public App(IServiceProvider serviceProvider)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
Initializes the application, applies the saved theme, and stores the service provider for later use.
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
1. Logs the start of constructor execution.
|
||||||
|
2. Calls `InitializeComponent()` to load XAML-defined UI elements.
|
||||||
|
3. Applies the saved user theme preference via `ApplySavedTheme()`.
|
||||||
|
4. Stores the injected `IServiceProvider`.
|
||||||
|
5. Catches and logs any fatal XAML initialization errors.
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### ApplySavedTheme()
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void ApplySavedTheme()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Summary**:
|
||||||
|
Applies the user's previously saved theme preference (Light, Dark, or System) to the application.
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- Retrieves the theme preference from `Preferences.Default` using the key `"AppTheme"`.
|
||||||
|
- Defaults to `"System"` if no preference is found.
|
||||||
|
- Maps the string preference to the corresponding `AppTheme` enum value.
|
||||||
|
- Sets `Application.Current.UserAppTheme`.
|
||||||
|
- Logs the applied theme or any failure (as a warning).
|
||||||
|
|
||||||
|
### CreateWindow(IActivationState? activationState)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override Window CreateWindow(IActivationState? activationState)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Summary**:
|
||||||
|
Overrides the default window creation to provide proper dependency injection support and robust error handling.
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- Logs entry into the method.
|
||||||
|
- Resolves `AppShell` using the injected `IServiceProvider`.
|
||||||
|
- Returns a new `Window` containing the resolved `AppShell`.
|
||||||
|
- If dependency resolution fails (e.g., missing service registration), a user-friendly error page is displayed instead of crashing the app.
|
||||||
|
- The fallback error page shows:
|
||||||
|
- "Startup Failed" title
|
||||||
|
- The exception message in red
|
||||||
|
- A note to check the log for details
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Theme Persistence**: Automatically restores the user's last chosen theme on startup.
|
||||||
|
- **Dependency Injection Ready**: Uses `IServiceProvider` to resolve the main `AppShell`.
|
||||||
|
- **Graceful Error Handling**: Critical startup failures are caught and presented in a clean error screen rather than a hard crash.
|
||||||
|
- **Comprehensive Logging**: Uses `AppLogger` for all major initialization steps and errors.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `Microsoft.Maui.Controls.Application`
|
||||||
|
- `Microsoft.Maui.Storage.Preferences`
|
||||||
|
- `IServiceProvider` (from Microsoft.Extensions.DependencyInjection)
|
||||||
|
- `AppShell`
|
||||||
|
- `AppLogger` (custom logging service)
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- `SettingsPage.xaml.cs` — Uses the same `ThemePrefKey` constant
|
||||||
|
- `AppShell.xaml` — Main navigation shell resolved in `CreateWindow`
|
||||||
|
- `MauiProgram.cs` — Where services are registered and `App` is configured
|
||||||
|
|
||||||
|
## Best Practices Implemented
|
||||||
|
|
||||||
|
- Separation of theme logic into a dedicated method
|
||||||
|
- Defensive programming with try-catch blocks around critical startup operations
|
||||||
|
- Meaningful logging at each stage of initialization
|
||||||
|
- Fallback UI for startup failures to improve user experience
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
# App.xaml Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is the main XAML file for the **FrymasterBadgeApp** .NET MAUI application. It defines the root `Application` object and sets up global resources that are available throughout the entire app.
|
||||||
|
|
||||||
|
## File Information
|
||||||
|
|
||||||
|
- **File**: `App.xaml`
|
||||||
|
- **Class**: `FrymasterBadgeApp.App` (partial class, paired with `App.xaml.cs`)
|
||||||
|
- **Purpose**: Application-level configuration and resource management
|
||||||
|
|
||||||
|
## XAML Content
|
||||||
|
|
||||||
|
\`\`\`xaml
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<Application
|
||||||
|
x:Class="FrymasterBadgeApp.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:local="clr-namespace:FrymasterBadgeApp">
|
||||||
|
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
||||||
|
|
||||||
|
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Key Elements
|
||||||
|
|
||||||
|
### Root Element
|
||||||
|
|
||||||
|
- **`<Application>`** — Defines the application root.
|
||||||
|
- **`x:Class="FrymasterBadgeApp.App"`** — Links this XAML file to the partial C# class `App` in `App.xaml.cs`.
|
||||||
|
|
||||||
|
### XML Namespaces
|
||||||
|
|
||||||
|
| Namespace | Prefix | Description |
|
||||||
|
|-----------|--------|-----------|
|
||||||
|
| `http://schemas.microsoft.com/dotnet/2021/maui` | (default) | Standard .NET MAUI namespace |
|
||||||
|
| `http://schemas.microsoft.com/winfx/2009/xaml` | `x` | XAML language namespace |
|
||||||
|
| `clr-namespace:FrymasterBadgeApp` | `local` | Local application namespace (currently unused in this file) |
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
The file defines **application-wide resources** inside `<Application.Resources>`:
|
||||||
|
|
||||||
|
- A `ResourceDictionary` that merges two external style files:
|
||||||
|
- **`Resources/Styles/Colors.xaml`** — Contains color definitions and brushes used across the app.
|
||||||
|
- **`Resources/Styles/Styles.xaml`** — Contains control styles, implicit styles, and other visual resources.
|
||||||
|
|
||||||
|
## Purpose & Role
|
||||||
|
|
||||||
|
- Serves as the central location for app-level resources.
|
||||||
|
- Ensures consistent theming and styling across all pages and controls.
|
||||||
|
- Loads color and style definitions early in the application lifecycle.
|
||||||
|
- Works together with `App.xaml.cs` (the code-behind) which handles logic such as theme application and window creation.
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- **`App.xaml.cs`** — Code-behind containing constructor, theme logic, and `CreateWindow` override.
|
||||||
|
- **`Resources/Styles/Colors.xaml`** — Color palette and resource definitions.
|
||||||
|
- **`Resources/Styles/Styles.xaml`** — Global styles and control templates.
|
||||||
|
- **`MauiProgram.cs`** — Service registration and app startup configuration.
|
||||||
|
|
||||||
|
## Best Practices Demonstrated
|
||||||
|
|
||||||
|
- Clean separation of concerns: XAML for resources, C# for logic.
|
||||||
|
- Use of `MergedDictionaries` for modular and maintainable styling.
|
||||||
|
- Proper namespace declarations for future extensibility.
|
||||||
|
- Standard .NET MAUI application structure.
|
||||||
|
|
||||||
|
This file is loaded automatically when the application starts, before any pages are displayed.
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace FrymasterBadgeApp;
|
||||||
|
|
||||||
|
public static class AppLogger
|
||||||
|
{
|
||||||
|
private static readonly string LogDirectory = Environment.GetFolderPath(
|
||||||
|
Environment.SpecialFolder.LocalApplicationData
|
||||||
|
);
|
||||||
|
private static readonly string BaseLogName = "FrymasterBadgeApp";
|
||||||
|
private static readonly long MaxFileSizeBytes = 5 * 1024 * 1024; // 5MB
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
static AppLogger()
|
||||||
|
{
|
||||||
|
Log("DEBUG", "AppLogger initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCurrentLogPath()
|
||||||
|
{
|
||||||
|
string date = DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
|
return Path.Combine(LogDirectory, $"{BaseLogName}_{date}.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Info(string message) => Log("INFO", message);
|
||||||
|
|
||||||
|
public static void Warn(string message) => Log("WARN", message);
|
||||||
|
|
||||||
|
public static void Warning(string message) => Log("WARNING", message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs an error to the application log, including the given message.
|
||||||
|
/// <param name="message">The message to be logged.</param name="ex">The exception associated with the error, or null if none.</summary>
|
||||||
|
public static void Error(string message, Exception? ex = null)
|
||||||
|
{
|
||||||
|
string full = ex != null ? $"{message}\nException: {ex}" : message;
|
||||||
|
Log("ERROR", full);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a a instructions to the application log, including the given message.
|
||||||
|
/// <param name="message">The message to be logged.</summary>
|
||||||
|
public static void Debug(string message)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Log("DEBUG", message);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message to the application log, including the given message.
|
||||||
|
/// </summary>
|
||||||
|
private static void Log(string level, string message)
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||||
|
string logLine = $"[{timestamp}] [{level}] {message}";
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
string logPath = GetCurrentLogPath();
|
||||||
|
|
||||||
|
// Rotate if too large
|
||||||
|
if (File.Exists(logPath) && new FileInfo(logPath).Length > MaxFileSizeBytes)
|
||||||
|
{
|
||||||
|
string backup = logPath + ".old";
|
||||||
|
if (File.Exists(backup))
|
||||||
|
File.Delete(backup);
|
||||||
|
File.Move(logPath, backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.AppendAllText(logPath, logLine + Environment.NewLine);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Log file write failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(logLine);
|
||||||
|
System.Diagnostics.Debug.WriteLine(logLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes old log files that are older than the given number of days.
|
||||||
|
/// </summary>
|
||||||
|
public static void CleanupOldLogs(int daysToKeep = 30)
|
||||||
|
{
|
||||||
|
var cutoff = DateTime.Now.AddDays(-daysToKeep);
|
||||||
|
foreach (var file in Directory.GetFiles(LogDirectory, $"{BaseLogName}_*.log"))
|
||||||
|
{
|
||||||
|
if (File.GetCreationTime(file) < cutoff)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,53 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<Shell
|
<Shell
|
||||||
|
x:Class="FrymasterBadgeApp.AppShell"
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:pages="clr-namespace:FrymasterBadgeApp"
|
xmlns:pages="clr-namespace:FrymasterBadgeApp"
|
||||||
x:Class="FrymasterBadgeApp.AppShell"
|
x:Name="thisShell"
|
||||||
>
|
Shell.FlyoutBehavior="Disabled"
|
||||||
<TabBar x:Name="MainTabBar" />
|
|
||||||
|
|
||||||
|
Shell.BackgroundColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource BgDark}}"
|
||||||
|
|
||||||
|
Shell.ForegroundColor="White"
|
||||||
|
Shell.TitleColor="White"
|
||||||
|
|
||||||
|
Shell.TabBarBackgroundColor="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource CardDark}}"
|
||||||
|
Shell.TabBarForegroundColor="{StaticResource Primary}"
|
||||||
|
Shell.TabBarTitleColor="{StaticResource Primary}"
|
||||||
|
Shell.TabBarUnselectedColor="{StaticResource Gray500}"
|
||||||
|
Shell.UnselectedColor="{StaticResource Gray500}">
|
||||||
|
|
||||||
|
<Shell.ToolbarItems>
|
||||||
|
<ToolbarItem
|
||||||
|
Clicked="OnSettingsClicked"
|
||||||
|
Order="Secondary"
|
||||||
|
Text="Settings" />
|
||||||
|
<ToolbarItem
|
||||||
|
Clicked="OnThemeClicked"
|
||||||
|
Order="Primary"
|
||||||
|
Text="Theme" />
|
||||||
|
<ToolbarItem
|
||||||
|
Clicked="OnManageCompaniesClicked"
|
||||||
|
Order="Secondary"
|
||||||
|
Text="Manage Companies" />
|
||||||
|
<ToolbarItem
|
||||||
|
Clicked="OnAboutClicked"
|
||||||
|
Order="Secondary"
|
||||||
|
Text="About" />
|
||||||
|
<ToolbarItem
|
||||||
|
Clicked="OnExitClicked"
|
||||||
|
Order="Secondary"
|
||||||
|
Text="Exit" />
|
||||||
|
</Shell.ToolbarItems>
|
||||||
|
|
||||||
|
<TabBar x:Name="MainTabBar" Route="MainTabBar">
|
||||||
|
<Tab x:Name="SetupTabContainer" Title="Setup">
|
||||||
<ShellContent
|
<ShellContent
|
||||||
x:Name="SetupTab"
|
x:Name="SetupTab"
|
||||||
Title="Setup"
|
Title="Setup"
|
||||||
ContentTemplate="{DataTemplate pages:CompanyPage}"
|
ContentTemplate="{DataTemplate pages:CompanyPage}"
|
||||||
/>
|
Route="CompanyPage" />
|
||||||
|
</Tab>
|
||||||
|
</TabBar>
|
||||||
</Shell>
|
</Shell>
|
||||||
148
AppShell.xaml.cs
148
AppShell.xaml.cs
|
|
@ -1,5 +1,6 @@
|
||||||
|
using System.Diagnostics;
|
||||||
using FrymasterBadgeApp.Services;
|
using FrymasterBadgeApp.Services;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Storage;
|
||||||
|
|
||||||
namespace FrymasterBadgeApp;
|
namespace FrymasterBadgeApp;
|
||||||
|
|
||||||
|
|
@ -7,66 +8,151 @@ public partial class AppShell : Shell
|
||||||
{
|
{
|
||||||
private readonly SqlService _db;
|
private readonly SqlService _db;
|
||||||
private readonly PrinterService _printerService;
|
private readonly PrinterService _printerService;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private bool _isInitialized = false;
|
||||||
|
|
||||||
public AppShell(SqlService db, PrinterService printerService)
|
/// <summary>
|
||||||
|
/// AppShell is a class that contains the initialization code for the app. It is responsible for
|
||||||
|
/// loading all companies in the background and then switching to the main thread to
|
||||||
|
/// update the UI.
|
||||||
|
/// </summary>
|
||||||
|
public AppShell(SqlService db, PrinterService printerService, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_db = db;
|
_db = db;
|
||||||
_printerService = printerService;
|
_printerService = printerService;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
|
||||||
_ = LoadCompaniesAsync();
|
// Register pages that are NOT part of the TabBar permanently
|
||||||
|
Routing.RegisterRoute(nameof(SettingsPage), typeof(SettingsPage));
|
||||||
|
Routing.RegisterRoute(nameof(CompanyPage), typeof(CompanyPage));
|
||||||
|
Routing.RegisterRoute(nameof(EmployeePage), typeof(EmployeePage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
|
||||||
|
// Using a small delay or ensuring initialization happens once
|
||||||
|
if (!_isInitialized)
|
||||||
|
{
|
||||||
|
_isInitialized = true;
|
||||||
|
await LoadCompaniesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all companies in the background and then switches to the main thread to update the UI.
|
||||||
|
/// </summary>
|
||||||
private async Task LoadCompaniesAsync()
|
private async Task LoadCompaniesAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 1. Fetch data in background
|
||||||
|
AppLogger.Debug("AppShell: Fetching companies");
|
||||||
var companies = await Task.Run(() => _db.Query("SELECT * FROM dbo.Companies", null));
|
var companies = await Task.Run(() => _db.Query("SELECT * FROM dbo.Companies", null));
|
||||||
|
|
||||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
// 2. Switch to Main Thread for UI changes
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
{
|
{
|
||||||
Items.Clear();
|
// Clear the "Loading" item
|
||||||
|
MainTabBar.Items.Clear();
|
||||||
|
|
||||||
if (companies == null || !companies.Any())
|
if (companies == null || !companies.Any())
|
||||||
{
|
{
|
||||||
CurrentItem = SetupTab;
|
MainTabBar.Items.Add(
|
||||||
return;
|
new ShellContent
|
||||||
|
{
|
||||||
|
Title = "Setup",
|
||||||
|
Route = "InitialSetup",
|
||||||
|
ContentTemplate = new DataTemplate(() =>
|
||||||
|
_serviceProvider.GetRequiredService<CompanyPage>()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
foreach (var company in companies)
|
foreach (var company in companies)
|
||||||
{
|
{
|
||||||
string companyName = company.GetValueOrDefault("Name")?.ToString() ?? "Unknown";
|
var companyId = company.GetValueOrDefault("ID")?.ToString() ?? "0";
|
||||||
|
MainTabBar.Items.Add(
|
||||||
var employeePage = new EmployeePage(_db, _printerService, company)
|
new ShellContent
|
||||||
{
|
{
|
||||||
Title = companyName,
|
Title = company.GetValueOrDefault("Name")?.ToString() ?? "Unknown",
|
||||||
};
|
Route = $"EmployeePage_{companyId}",
|
||||||
|
ContentTemplate = new DataTemplate(() =>
|
||||||
var manageBtn = new ToolbarItem
|
new EmployeePage(_db, _printerService, company)
|
||||||
{
|
|
||||||
Text = "Manage Companies",
|
|
||||||
Order = ToolbarItemOrder.Primary,
|
|
||||||
Command = new Command(async () =>
|
|
||||||
await employeePage.Navigation.PushAsync(new CompanyPage(_db))
|
|
||||||
),
|
),
|
||||||
};
|
}
|
||||||
employeePage.ToolbarItems.Add(manageBtn);
|
);
|
||||||
|
}
|
||||||
var tab = new Tab { Title = companyName };
|
|
||||||
tab.Items.Add(new ShellContent { Content = employeePage });
|
|
||||||
|
|
||||||
MainTabBar.Items.Add(tab);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Navigate away from the Loading page to the first real tab
|
||||||
if (MainTabBar.Items.Count > 0)
|
if (MainTabBar.Items.Count > 0)
|
||||||
CurrentItem = MainTabBar.Items[0];
|
{
|
||||||
|
var targetRoute = MainTabBar.Items[0].Route;
|
||||||
|
// The "///" is critical—it tells Shell to rebuild the navigation stack
|
||||||
|
await Shell.Current.GoToAsync($"///{targetRoute}");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
AppLogger.Error("AppShell: Error loading companies", ex);
|
||||||
await DisplayAlert("Error", $"Failed to load: {ex.Message}", "OK")
|
// Fallback: If DB fails, at least show the Company page
|
||||||
);
|
await Shell.Current.GoToAsync($"///InitialSetup");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigates to CompanyPage as a sub-page (pushed onto the stack).
|
||||||
|
public async void OnManageCompaniesClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Navigates to CompanyPage as a sub-page (pushed onto the stack)
|
||||||
|
await Shell.Current.GoToAsync(nameof(CompanyPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSettingsClicked(object sender, EventArgs e) =>
|
||||||
|
await Shell.Current.GoToAsync(nameof(SettingsPage));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open a simple action sheet to choose theme globally
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">The object that triggered the event</param>
|
||||||
|
/// <param name="e">The event arguments</param>
|
||||||
|
private async void OnThemeClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Open a simple action sheet to choose theme globally
|
||||||
|
string action = await Shell.Current.DisplayActionSheet("Select Theme", "Cancel", null, "System", "Light", "Dark");
|
||||||
|
if (action == "Cancel" || string.IsNullOrEmpty(action))
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case "Light":
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Light;
|
||||||
|
Preferences.Default.Set("AppTheme", "Light");
|
||||||
|
break;
|
||||||
|
case "Dark":
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Dark;
|
||||||
|
Preferences.Default.Set("AppTheme", "Dark");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Unspecified;
|
||||||
|
Preferences.Default.Set("AppTheme", "System");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExitClicked(object sender, EventArgs e) => Application.Current?.Quit();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open a simple action sheet to show the app's about page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">The object that triggered the event</param>
|
||||||
|
/// <param name="e">The event arguments</param>
|
||||||
|
private async void OnAboutClicked(object sender, EventArgs e) =>
|
||||||
|
await DisplayAlert("About", "Frymaster Badge App v1.0", "OK");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
224
CompanyPage.xaml
224
CompanyPage.xaml
|
|
@ -1,124 +1,168 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<ContentPage
|
<ContentPage
|
||||||
|
x:Class="FrymasterBadgeApp.CompanyPage"
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="FrymasterBadgeApp.CompanyPage"
|
Title="Company Settings">
|
||||||
Title="Manage Companies"
|
|
||||||
BackgroundColor="#f1f5f9"
|
<Grid ColumnDefinitions="350, *">
|
||||||
>
|
<Border
|
||||||
<Grid ColumnDefinitions="350,*" Padding="20">
|
Grid.Column="0"
|
||||||
<!-- Left: Company List -->
|
Margin="10"
|
||||||
<Frame
|
Style="{StaticResource CardStyle}">
|
||||||
BackgroundColor="White"
|
<VerticalStackLayout Padding="10" Spacing="10">
|
||||||
BorderColor="#e2e8f0"
|
|
||||||
CornerRadius="12"
|
|
||||||
HasShadow="True"
|
|
||||||
Padding="0"
|
|
||||||
>
|
|
||||||
<VerticalStackLayout>
|
|
||||||
<Label
|
<Label
|
||||||
Text="Companies"
|
|
||||||
FontSize="18"
|
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
Margin="15"
|
FontSize="18"
|
||||||
TextColor="#1e293b"
|
Text="Registered Companies" />
|
||||||
/>
|
<Button
|
||||||
|
BackgroundColor="{StaticResource Success}"
|
||||||
|
Clicked="OnAddNewClicked"
|
||||||
|
Text="+ Add New"
|
||||||
|
TextColor="White" />
|
||||||
|
|
||||||
<CollectionView
|
<CollectionView
|
||||||
x:Name="CompanyList"
|
x:Name="CompanyList"
|
||||||
SelectionMode="Single"
|
SelectionChanged="OnCompanySelectionChanged"
|
||||||
SelectionChanged="OnCompanySelected"
|
SelectionMode="Single">
|
||||||
Margin="10"
|
|
||||||
>
|
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Frame BackgroundColor="#f8fafc" CornerRadius="8" Padding="12" Margin="5">
|
<Grid Padding="10">
|
||||||
<Label Text="{Binding [Name]}" FontSize="16" TextColor="#1e293b" />
|
<Label
|
||||||
</Frame>
|
FontSize="16"
|
||||||
|
Text="{Binding [Name]}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Frame>
|
</Border>
|
||||||
|
|
||||||
<!-- Right: Edit Form -->
|
<ScrollView Grid.Column="1">
|
||||||
<ScrollView Grid.Column="1" Margin="30,0,0,0">
|
<VerticalStackLayout Padding="30" Spacing="25">
|
||||||
<VerticalStackLayout Spacing="25" HorizontalOptions="Start" WidthRequest="700">
|
<Label
|
||||||
<Button
|
|
||||||
Text="+ Add New Company"
|
|
||||||
Clicked="OnAddNewClicked"
|
|
||||||
BackgroundColor="#10b981"
|
|
||||||
TextColor="White"
|
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
CornerRadius="12"
|
FontSize="24"
|
||||||
HeightRequest="50"
|
Text="Edit Company Details"
|
||||||
/>
|
TextColor="{StaticResource Primary}" />
|
||||||
|
|
||||||
<Frame BackgroundColor="White" CornerRadius="16" HasShadow="True" Padding="30">
|
<Border Padding="25" Style="{StaticResource CardStyle}">
|
||||||
<VerticalStackLayout Spacing="20">
|
<VerticalStackLayout Spacing="20">
|
||||||
<Label Text="Company Logo" FontSize="16" FontAttributes="Bold" TextColor="#1e293b" />
|
|
||||||
|
|
||||||
<HorizontalStackLayout HorizontalOptions="Center" Spacing="20">
|
<Grid ColumnDefinitions="100, *" ColumnSpacing="15">
|
||||||
<Frame BackgroundColor="#f1f5f9" CornerRadius="8" Padding="0">
|
<VerticalStackLayout Grid.Column="1" Spacing="5">
|
||||||
<Image
|
<Label
|
||||||
x:Name="LogoPreview"
|
FontAttributes="Bold"
|
||||||
HeightRequest="120"
|
FontSize="12"
|
||||||
WidthRequest="240"
|
Text="Company Name"
|
||||||
Aspect="AspectFit"
|
TextColor="{StaticResource Primary}" />
|
||||||
/>
|
<Entry x:Name="CompName" Placeholder="Required" />
|
||||||
</Frame>
|
</VerticalStackLayout>
|
||||||
</HorizontalStackLayout>
|
</Grid>
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="5">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="Street Address"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Entry x:Name="CompAddress" Placeholder="123 Main St" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*, 100, 150" ColumnSpacing="15">
|
||||||
|
<VerticalStackLayout Grid.Column="0" Spacing="5">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="City"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Entry x:Name="CompCity" Placeholder="City Name" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<VerticalStackLayout Grid.Column="1" Spacing="5">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="State"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Entry
|
||||||
|
x:Name="CompState"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
MaxLength="2"
|
||||||
|
Placeholder="ST" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<VerticalStackLayout Grid.Column="2" Spacing="5">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="Zip Code"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Entry
|
||||||
|
x:Name="CompZip"
|
||||||
|
Keyboard="Numeric"
|
||||||
|
Placeholder="12345" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="10">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="Company Logo"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Grid ColumnDefinitions="Auto, *" ColumnSpacing="20">
|
||||||
|
<Border
|
||||||
|
Grid.Column="0"
|
||||||
|
Padding="5"
|
||||||
|
HeightRequest="100"
|
||||||
|
Style="{StaticResource CardStyle}"
|
||||||
|
WidthRequest="100">
|
||||||
|
<Image x:Name="LogoPreview" Aspect="AspectFit" />
|
||||||
|
</Border>
|
||||||
|
<VerticalStackLayout
|
||||||
|
Grid.Column="1"
|
||||||
|
Spacing="10"
|
||||||
|
VerticalOptions="Center">
|
||||||
|
<Label
|
||||||
|
x:Name="LogoPathLabel"
|
||||||
|
FontSize="11"
|
||||||
|
Text="No logo selected"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
<Button
|
<Button
|
||||||
Text="Choose Logo Image"
|
BackgroundColor="{StaticResource Secondary}"
|
||||||
Clicked="OnSelectLogoClicked"
|
Clicked="OnSelectLogoClicked"
|
||||||
BackgroundColor="#64748b"
|
HorizontalOptions="Start"
|
||||||
TextColor="White"
|
Text="Browse for Logo"
|
||||||
CornerRadius="10"
|
TextColor="White" />
|
||||||
/>
|
|
||||||
|
|
||||||
<VerticalStackLayout Spacing="15">
|
|
||||||
<Label Text="Company Name" TextColor="#475569" />
|
|
||||||
<Entry x:Name="CompName" Placeholder="Enter name" />
|
|
||||||
|
|
||||||
<Label Text="Street Address" TextColor="#475569" />
|
|
||||||
<Entry x:Name="CompAddress" Placeholder="Enter address" />
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,100,150" ColumnSpacing="15">
|
|
||||||
<VerticalStackLayout Grid.Column="0">
|
|
||||||
<Label Text="City" TextColor="#475569" />
|
|
||||||
<Entry x:Name="CompCity" Placeholder="City" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
<VerticalStackLayout Grid.Column="1">
|
|
||||||
<Label Text="State" TextColor="#475569" />
|
|
||||||
<Entry x:Name="CompState" Placeholder="ST" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
<VerticalStackLayout Grid.Column="2">
|
|
||||||
<Label Text="Zip" TextColor="#475569" />
|
|
||||||
<Entry x:Name="CompZip" Placeholder="Zip" />
|
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Grid>
|
</Grid>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="20">
|
<BoxView
|
||||||
|
Margin="0,10"
|
||||||
|
HeightRequest="1"
|
||||||
|
Color="{StaticResource Gray500}" />
|
||||||
|
|
||||||
|
<HorizontalStackLayout HorizontalOptions="End" Spacing="15">
|
||||||
<Button
|
<Button
|
||||||
Text="Save Company"
|
x:Name="DeleteBtn"
|
||||||
Clicked="OnSaveClicked"
|
BackgroundColor="{StaticResource Danger}"
|
||||||
BackgroundColor="#2563eb"
|
|
||||||
TextColor="White"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
CornerRadius="12"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
Text="Delete Company"
|
|
||||||
Clicked="OnDeleteClicked"
|
Clicked="OnDeleteClicked"
|
||||||
BackgroundColor="#ef4444"
|
Text="Delete Company"
|
||||||
TextColor="White"
|
TextColor="White"
|
||||||
|
WidthRequest="150" />
|
||||||
|
<Button
|
||||||
|
BackgroundColor="{StaticResource Success}"
|
||||||
|
Clicked="OnSaveClicked"
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
CornerRadius="12"
|
Text="Save Changes"
|
||||||
/>
|
TextColor="White"
|
||||||
</Grid>
|
WidthRequest="200" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Frame>
|
</Border>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
using System.Diagnostics;
|
||||||
using FrymasterBadgeApp.Services;
|
using FrymasterBadgeApp.Services;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Maui.Storage;
|
||||||
|
|
||||||
namespace FrymasterBadgeApp;
|
namespace FrymasterBadgeApp;
|
||||||
|
|
||||||
|
|
@ -8,102 +10,293 @@ public partial class CompanyPage : ContentPage
|
||||||
private readonly SqlService _db;
|
private readonly SqlService _db;
|
||||||
private Dictionary<string, object>? _selectedCompany;
|
private Dictionary<string, object>? _selectedCompany;
|
||||||
private string _tempLogoPath = "";
|
private string _tempLogoPath = "";
|
||||||
|
private const string LogoPathKey = "LogoBasePath";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for CompanyPage.
|
||||||
|
/// of type ContentPage.
|
||||||
|
/// the main page for viewing and editing company information.
|
||||||
|
/// <param name="db">The SQLService object used to access the database.</param>
|
||||||
public CompanyPage(SqlService db)
|
public CompanyPage(SqlService db)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_db = db;
|
_db = db;
|
||||||
|
|
||||||
|
Shell.SetBackButtonBehavior(
|
||||||
|
this,
|
||||||
|
new BackButtonBehavior
|
||||||
|
{
|
||||||
|
TextOverride = "Back",
|
||||||
|
Command = new Command(async () => await Shell.Current.GoToAsync("..")),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
LoadCompanies();
|
LoadCompanies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the path to the directory used to store company logos.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The path to the directory used to store company logos.</returns>
|
||||||
private string GetLogoDirectory() =>
|
private string GetLogoDirectory() =>
|
||||||
Path.Combine(
|
Preferences.Default.Get(LogoPathKey, @"C:\FrymasterData\logos");
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
|
||||||
"FrymasterBadgeApp",
|
|
||||||
"Logos"
|
|
||||||
);
|
|
||||||
|
|
||||||
private void LoadCompanies() =>
|
/// <summary>
|
||||||
CompanyList.ItemsSource = _db.Query("SELECT * FROM Companies", null);
|
/// Loads the list of companies from the database.
|
||||||
|
/// the results are ordered by company name (ascending).
|
||||||
private void OnCompanySelected(object sender, SelectedItemChangedEventArgs e)
|
/// and displays the companies are stored in the CompanyList.
|
||||||
|
/// </summary>
|
||||||
|
private void LoadCompanies()
|
||||||
{
|
{
|
||||||
_selectedCompany = e.SelectedItem as Dictionary<string, object>;
|
try
|
||||||
if (_selectedCompany == null)
|
{
|
||||||
return;
|
var companies = _db.Query("SELECT * FROM Companies ORDER BY Name ASC", null);
|
||||||
|
CompanyList.ItemsSource = companies;
|
||||||
CompName.Text = _selectedCompany["Name"]?.ToString();
|
}
|
||||||
CompAddress.Text = _selectedCompany["Address"]?.ToString();
|
catch (Exception ex)
|
||||||
_tempLogoPath = _selectedCompany.GetValueOrDefault("logo")?.ToString() ?? "";
|
{
|
||||||
LogoPreview.Source = File.Exists(_tempLogoPath)
|
AppLogger.Error("Failed to load companies", ex);
|
||||||
? ImageSource.FromFile(_tempLogoPath)
|
DisplayAlert("Error", "Failed to load companies.", "OK");
|
||||||
: null;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the selection in the companies list changes.
|
||||||
|
/// the selection is used to update the company name, address, city, state, zip code, and delete button visibility.
|
||||||
|
/// </summary>
|
||||||
|
private void OnCompanySelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var selected = e.CurrentSelection.FirstOrDefault() as Dictionary<string, object>;
|
||||||
|
if (selected == null)
|
||||||
|
{
|
||||||
|
ClearFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedCompany = selected;
|
||||||
|
|
||||||
|
MainThread.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
CompName.Text = GetValue(selected, "Name");
|
||||||
|
CompAddress.Text = GetValue(selected, "Address");
|
||||||
|
CompCity.Text = GetValue(selected, "City");
|
||||||
|
CompState.Text = GetValue(selected, "State");
|
||||||
|
CompZip.Text = GetValue(selected, "Zip");
|
||||||
|
DeleteBtn.IsVisible = true;
|
||||||
|
|
||||||
|
LogoPreview.Source = null;
|
||||||
|
_tempLogoPath = "";
|
||||||
|
|
||||||
|
// Get stored filename only
|
||||||
|
string storedFileName = GetValue(selected, "logo");
|
||||||
|
if (!string.IsNullOrWhiteSpace(storedFileName))
|
||||||
|
{
|
||||||
|
string logoDir = GetLogoDirectory();
|
||||||
|
string fullPath = Path.Combine(logoDir, storedFileName);
|
||||||
|
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
_tempLogoPath = fullPath;
|
||||||
|
LogoPreview.Source = ImageSource.FromFile(fullPath);
|
||||||
|
AppLogger.Info($"Logo loaded for company: {fullPath}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppLogger.Warning($"Logo file missing: {fullPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the value of the given key from the dictionary, trimmed of whitespace.
|
||||||
|
/// If the key does not exist, an empty string is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dict">The dictionary to search.</param>
|
||||||
|
/// <param name="key">The key to search for.</param>
|
||||||
|
/// <returns>The value of the given key, trimmed of whitespace, or an empty string if the key does not exist.</returns>
|
||||||
|
private string GetValue(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
var actualKey = dict.Keys.FirstOrDefault(k =>
|
||||||
|
k.Equals(key, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
return actualKey != null ? dict[actualKey]?.ToString()?.Trim() ?? "" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the user selects a logo image from the file picker.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">The object that triggered the event</param>
|
||||||
|
/// <param name="e">The event data which contains information about the event</param>
|
||||||
private async void OnSelectLogoClicked(object sender, EventArgs e)
|
private async void OnSelectLogoClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var result = await FilePicker.Default.PickAsync(
|
var result = await FilePicker.Default.PickAsync(
|
||||||
new PickOptions { PickerTitle = "Select Logo", FileTypes = FilePickerFileType.Images }
|
new PickOptions
|
||||||
|
{
|
||||||
|
PickerTitle = "Select Company Logo",
|
||||||
|
FileTypes = FilePickerFileType.Images,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
_tempLogoPath = result.FullPath;
|
_tempLogoPath = result.FullPath;
|
||||||
LogoPreview.Source = ImageSource.FromFile(_tempLogoPath);
|
LogoPreview.Source = ImageSource.FromFile(_tempLogoPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
private void OnSaveClicked(object sender, EventArgs e)
|
|
||||||
{
|
{
|
||||||
|
await DisplayAlert("Error", $"Failed to select logo: {ex.Message}", "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save the selected company to the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">The object that invoked this method.</param>
|
||||||
|
/// <param name="e">The event data for this method.</param>
|
||||||
|
private async void OnSaveClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CompName.Text))
|
||||||
|
{
|
||||||
|
await DisplayAlert("Error", "Company Name is required", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string logoDir = GetLogoDirectory();
|
string logoDir = GetLogoDirectory();
|
||||||
if (!Directory.Exists(logoDir))
|
if (!Directory.Exists(logoDir))
|
||||||
Directory.CreateDirectory(logoDir);
|
|
||||||
|
|
||||||
string finalLogoPath = _tempLogoPath;
|
|
||||||
if (!string.IsNullOrEmpty(_tempLogoPath) && !_tempLogoPath.Contains(logoDir))
|
|
||||||
{
|
{
|
||||||
string dest = Path.Combine(logoDir, Path.GetFileName(_tempLogoPath));
|
try
|
||||||
File.Copy(_tempLogoPath, dest, true);
|
{
|
||||||
finalLogoPath = dest;
|
Directory.CreateDirectory(logoDir);
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await DisplayAlert("Error", "Cannot create logo directory.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string logoFileNameToSave = ""; // will be stored in DB
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_tempLogoPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string originalFileName = Path.GetFileName(_tempLogoPath);
|
||||||
|
string extension = Path.GetExtension(originalFileName);
|
||||||
|
string baseName = Path.GetFileNameWithoutExtension(originalFileName);
|
||||||
|
|
||||||
|
// Avoid overwrites: add timestamp if file already exists
|
||||||
|
string destFileName = originalFileName;
|
||||||
|
string destPath = Path.Combine(logoDir, destFileName);
|
||||||
|
|
||||||
|
if (File.Exists(destPath))
|
||||||
|
{
|
||||||
|
destFileName = $"{baseName}_{DateTime.Now:yyyyMMdd_HHmmss}{extension}";
|
||||||
|
destPath = Path.Combine(logoDir, destFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(_tempLogoPath, destPath, true);
|
||||||
|
logoFileNameToSave = destFileName; // only filename saved to DB
|
||||||
|
_tempLogoPath = destPath; // update local temp for preview
|
||||||
|
|
||||||
|
AppLogger.Info($"Logo saved as: {destPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error("Failed to copy logo", ex);
|
||||||
|
await DisplayAlert("Error", "Failed to save logo file.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parameters = new List<SqlParameter>
|
||||||
|
{
|
||||||
|
new("@n", CompName.Text ?? ""),
|
||||||
|
new("@a", CompAddress.Text ?? ""),
|
||||||
|
new("@c", CompCity.Text ?? ""),
|
||||||
|
new("@s", CompState.Text ?? ""),
|
||||||
|
new("@z", CompZip.Text ?? ""),
|
||||||
|
new("@l", logoFileNameToSave), // ← only filename
|
||||||
|
};
|
||||||
|
|
||||||
if (_selectedCompany == null)
|
if (_selectedCompany == null)
|
||||||
{
|
{
|
||||||
_db.Execute(
|
_db.Execute(
|
||||||
"INSERT INTO Companies (Name, Address, logo) VALUES (@n, @a, @l)",
|
"INSERT INTO Companies (Name, Address, City, State, Zip, logo) VALUES (@n, @a, @c, @s, @z, @l)",
|
||||||
new SqlParameter[]
|
parameters.ToArray()
|
||||||
{
|
|
||||||
new("@n", CompName.Text),
|
|
||||||
new("@a", CompAddress.Text),
|
|
||||||
new("@l", finalLogoPath),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
await DisplayAlert("Success", "Company added.", "OK");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
parameters.Add(new("@id", _selectedCompany["ID"]));
|
||||||
_db.Execute(
|
_db.Execute(
|
||||||
"UPDATE Companies SET Name=@n, Address=@a, logo=@l WHERE ID=@id",
|
"UPDATE Companies SET Name=@n, Address=@a, City=@c, State=@s, Zip=@z, logo=@l WHERE ID=@id",
|
||||||
new SqlParameter[]
|
parameters.ToArray()
|
||||||
{
|
|
||||||
new("@n", CompName.Text),
|
|
||||||
new("@a", CompAddress.Text),
|
|
||||||
new("@l", finalLogoPath),
|
|
||||||
new("@id", _selectedCompany["ID"]),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
await DisplayAlert("Success", "Company updated.", "OK");
|
||||||
LoadCompanies();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadCompanies();
|
||||||
|
ClearFields();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error("Database save failed", ex);
|
||||||
|
await DisplayAlert("Database Error", ex.Message, "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the UI state after clicking the "Add New" button.
|
||||||
|
/// </summary>
|
||||||
private void OnAddNewClicked(object sender, EventArgs e)
|
private void OnAddNewClicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_selectedCompany = null;
|
CompanyList.SelectedItem = null;
|
||||||
CompName.Text = CompAddress.Text = "";
|
ClearFields();
|
||||||
LogoPreview.Source = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDeleteClicked(
|
/// <summary>
|
||||||
object sender,
|
/// Resets the UI state after clicking the "Add New" button or deleting a company.
|
||||||
EventArgs e
|
/// </summary>
|
||||||
) { /* Delete logic */
|
private void ClearFields()
|
||||||
|
{
|
||||||
|
_selectedCompany = null;
|
||||||
|
_tempLogoPath = "";
|
||||||
|
CompName.Text = CompAddress.Text = CompCity.Text = CompState.Text = CompZip.Text = "";
|
||||||
|
LogoPreview.Source = null;
|
||||||
|
DeleteBtn.IsVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a company from the database.
|
||||||
|
/// </summary>
|
||||||
|
private async void OnDeleteClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_selectedCompany == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (await DisplayAlert("Confirm", $"Delete {CompName.Text}?", "Yes", "No"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Execute(
|
||||||
|
"DELETE FROM Companies WHERE ID=@id",
|
||||||
|
new SqlParameter[] { new("@id", _selectedCompany["ID"]) }
|
||||||
|
);
|
||||||
|
LoadCompanies();
|
||||||
|
ClearFields();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await DisplayAlert("Error", ex.Message, "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
|
namespace FrymasterBadgeApp.Converters;
|
||||||
|
|
||||||
|
public class IsNotNullConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(
|
||||||
|
object value,
|
||||||
|
Type targetType,
|
||||||
|
object parameter,
|
||||||
|
System.Globalization.CultureInfo culture
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return value != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(
|
||||||
|
object value,
|
||||||
|
Type targetType,
|
||||||
|
object parameter,
|
||||||
|
System.Globalization.CultureInfo culture
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# App
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `Microsoft.Maui.Controls.Application`
|
||||||
|
**Purpose:** MAUI application entry point. Handles theme persistence, dependency injection, and graceful startup error handling.
|
||||||
|
|
||||||
|
## 🔑 Fields
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `_serviceProvider` | `IServiceProvider` | `private` | Holds the DI container used to resolve services like `AppShell`. |
|
||||||
|
| `ThemePrefKey` | `const string` | `public` | Constant key (`"AppTheme"`) used to store/retrieve theme preference. Must match `SettingsPage`. |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `App(IServiceProvider serviceProvider)`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Parameters:** `serviceProvider` (`IServiceProvider`)
|
||||||
|
- **Behavior:**
|
||||||
|
1. Logs constructor start.
|
||||||
|
2. Calls `InitializeComponent()` to load XAML UI.
|
||||||
|
3. Applies saved theme via `ApplySavedTheme()`.
|
||||||
|
4. Stores the injected `IServiceProvider`.
|
||||||
|
5. Catches and logs fatal XAML initialization errors.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `ApplySavedTheme()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Returns:** `void`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Reads `"AppTheme"` from `Preferences.Default`. Defaults to `"System"`.
|
||||||
|
2. Maps string to `AppTheme` enum (`Light`, `Dark`, `Unspecified`).
|
||||||
|
3. Sets `Application.Current.UserAppTheme`.
|
||||||
|
4. Logs success or warns on failure.
|
||||||
|
|
||||||
|
### `CreateWindow(IActivationState? activationState)`
|
||||||
|
- **Access:** `protected override`
|
||||||
|
- **Parameters:** `activationState` (`IActivationState?`)
|
||||||
|
- **Returns:** `Window`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Resolves `AppShell` via `_serviceProvider`.
|
||||||
|
2. Returns new `Window` containing resolved `AppShell`.
|
||||||
|
3. If resolution fails, returns a fallback `ContentPage` with `"Startup Failed"` title, exception message in red, and log reference. Prevents hard crash.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Key Features:**
|
||||||
|
- ✅ Theme persistence across sessions
|
||||||
|
- ✅ DI-ready shell resolution
|
||||||
|
- ✅ Graceful startup failure UI
|
||||||
|
- ✅ Comprehensive initialization logging
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
# AppLogger
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Type:** `static class`
|
||||||
|
**Purpose:** Centralized, thread-safe logging utility that writes timestamped messages to daily rotating log files, console, and debug output. Includes automatic file rotation and scheduled cleanup.
|
||||||
|
|
||||||
|
## 🔑 Fields
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `LogDirectory` | `string` | `private static readonly` | Path to `%LOCALAPPDATA%` where logs are stored. |
|
||||||
|
| `BaseLogName` | `string` | `private static readonly` | Base filename prefix (`FrymasterBadgeApp`). |
|
||||||
|
| `MaxFileSizeBytes` | `long` | `private static readonly` | Maximum log file size before rotation (`5 MB`). |
|
||||||
|
| `_lock` | `object` | `private static readonly` | Synchronization object for thread-safe file writes. |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `static AppLogger()`
|
||||||
|
- **Access:** `static`
|
||||||
|
- **Behavior:** Automatically logs a `DEBUG` message upon first access to confirm initialization.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `GetCurrentLogPath()`
|
||||||
|
- **Access:** `private static`
|
||||||
|
- **Returns:** `string`
|
||||||
|
- **Behavior:** Constructs a log file path using the current date in `yyyy-MM-dd` format (e.g., `...\FrymasterBadgeApp_2026-04-13.log`).
|
||||||
|
|
||||||
|
### `Info(string message)`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Parameters:**
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `message` | `string` | The informational text to log. |
|
||||||
|
- **Behavior:** Wrapper that calls `Log("INFO ", message)`.
|
||||||
|
|
||||||
|
### `Warn(string message)`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Parameters:** `message` (`string`)
|
||||||
|
- **Behavior:** Logs a warning-level message (`"WARN "`).
|
||||||
|
|
||||||
|
### `Warning(string message)`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Parameters:** `message` (`string`)
|
||||||
|
- **Behavior:** Alias for `Warn`, logs `"WARNING "` level.
|
||||||
|
|
||||||
|
### `Error(string message, Exception? ex = null)`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Parameters:**
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `message` | `string` | Primary error description. |
|
||||||
|
| `ex` | `Exception?` | Associated exception (optional). |
|
||||||
|
- **Behavior:** Appends `\nException: {ex}` if `ex` is provided, then logs at `"ERROR "` level.
|
||||||
|
|
||||||
|
### `Debug(string message)`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Parameters:** `message` (`string`)
|
||||||
|
- **Behavior:** Logs at `"DEBUG"` level. Wrapped in `#if DEBUG`, so calls are compiled out in Release builds.
|
||||||
|
|
||||||
|
### `Log(string level, string message)`
|
||||||
|
- **Access:** `private static`
|
||||||
|
- **Parameters:**
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `level` | `string` | Log level string (`INFO`, `ERROR`, etc.) |
|
||||||
|
| `message` | `string` | Log content. |
|
||||||
|
- **Behavior:**
|
||||||
|
1. Formats timestamp (`yyyy-MM-dd HH:mm:ss.fff`).
|
||||||
|
2. Acquires `_lock` for thread safety.
|
||||||
|
3. Checks if current log exceeds `MaxFileSizeBytes`. If so, renames to `.old` (deleting existing `.old` first).
|
||||||
|
4. Appends formatted line to file. Catches & logs `Console.WriteLine` if file I/O fails.
|
||||||
|
5. Writes to `Console` and `System.Diagnostics.Debug`.
|
||||||
|
|
||||||
|
### `CleanupOldLogs(int daysToKeep = 30)`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Parameters:** `daysToKeep` (`int`, default: `30`)
|
||||||
|
- **Behavior:** Scans `LogDirectory` for `FrymasterBadgeApp_*.log`, checks `File.GetCreationTime`, and deletes expired files. Exceptions during deletion are silently caught.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Notes:**
|
||||||
|
- Thread-safe via `lock (_lock)`.
|
||||||
|
- Log rotation prevents unbounded disk growth.
|
||||||
|
- Safe to call from any thread.
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# AppShell
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `Shell`
|
||||||
|
**Purpose:** MAUI Shell container responsible for dynamic tab generation, routing, theme switching, and app-level navigation.
|
||||||
|
|
||||||
|
## 🔑 Fields
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `_db` | `SqlService` | `private readonly` | Database service. |
|
||||||
|
| `_printerService` | `PrinterService` | `private readonly` | Printing service. |
|
||||||
|
| `_serviceProvider` | `IServiceProvider` | `private readonly` | DI container for resolving pages. |
|
||||||
|
| `_isInitialized` | `bool` | `private` | Prevents duplicate company loading. |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `AppShell(SqlService db, PrinterService printerService, IServiceProvider serviceProvider)`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Behavior:** Registers routes for `SettingsPage`, `CompanyPage`, and `EmployeePage`.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `OnAppearing()`
|
||||||
|
- **Access:** `protected override async void`
|
||||||
|
- **Behavior:** Ensures `LoadCompaniesAsync()` runs exactly once when shell becomes visible.
|
||||||
|
|
||||||
|
### `LoadCompaniesAsync()`
|
||||||
|
- **Access:** `private async Task`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Fetches companies on background thread via `Task.Run`.
|
||||||
|
2. Switches to main thread to clear `MainTabBar.Items`.
|
||||||
|
3. If no companies: adds `"Setup"` tab routing to `CompanyPage`.
|
||||||
|
4. Else: adds `ShellContent` tab per company routing to `EmployeePage_{ID}`.
|
||||||
|
5. Navigates to first tab using `///{targetRoute}` (rebuilds stack).
|
||||||
|
6. Falls back to `///InitialSetup` on DB failure.
|
||||||
|
|
||||||
|
### `OnManageCompaniesClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `public async void`
|
||||||
|
- **Behavior:** Pushes `CompanyPage` onto navigation stack.
|
||||||
|
|
||||||
|
### `OnSettingsClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Pushes `SettingsPage` onto stack.
|
||||||
|
|
||||||
|
### `OnThemeClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Displays action sheet (`System`, `Light`, `Dark`). Updates `Application.Current.UserAppTheme` and persists to `Preferences.Default`.
|
||||||
|
|
||||||
|
### `OnExitClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Calls `Application.Current?.Quit()`.
|
||||||
|
|
||||||
|
### `OnAboutClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Displays alert: `"Frymaster Badge App v1.0"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Architecture Notes:**
|
||||||
|
- Uses `///` route prefix to force full navigation stack rebuild when switching initial tabs.
|
||||||
|
- Thread-safe UI updates via `MainThread.InvokeOnMainThreadAsync`.
|
||||||
|
- Graceful degradation to setup screen on database failure.
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# EmployeeFormPage
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `ContentPage`
|
||||||
|
**Purpose:** Modal page for creating or editing employee records. Handles form binding, photo capture/cropping/zooming, badge generation, and SQL persistence.
|
||||||
|
|
||||||
|
## 🔑 Fields & Properties
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `_db` | `SqlService` | `private readonly` | Database access service. |
|
||||||
|
| `_existingEmployee` | `Dictionary<string, object>?` | `private readonly` | Holds employee data when in edit mode. |
|
||||||
|
| `OnSavedCallback` | `Action<Dictionary<string, object>>` | `public` | Callback triggered after successful save. |
|
||||||
|
| `_isEditMode` | `bool` | `private readonly` | Determines if page is adding or updating. |
|
||||||
|
| `_tempCapturePath` | `string` | `private` | Path to temporarily cached camera image. |
|
||||||
|
| `_editX`, `_editY` | `double` | `private` | Current pan offset for photo cropping. |
|
||||||
|
| `_photosBasePath` | `string` | `private readonly` | Directory for employee photos (from Preferences). |
|
||||||
|
| `IsSaved` | `bool` | `public` | Tracks if save operation completed successfully. |
|
||||||
|
| `SavedEmployee` | `Dictionary<string, object>?` | `public` | Stores minimal data of the saved record. |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `EmployeeFormPage(SqlService db, Dictionary<string, object>? employee = null)`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Parameters:** `db` (`SqlService`), `employee` (`Dictionary?`, optional)
|
||||||
|
- **Behavior:** Binds `ZoomSlider` to `Image.ScaleProperty`. Routes to `SetupEditMode` or `SetupAddMode` based on `employee` parameter.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `SetupEditMode(Dictionary<string, object> employee)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Populates form fields, disables `Data1Entry`, parses dates/booleans safely, loads photo if exists, and applies saved crop/zoom state.
|
||||||
|
|
||||||
|
### `SetupAddMode()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Resets form: title `"Add Employee"`, clears `Data1Entry`, sets date to today, checks `ActiveCheckBox`.
|
||||||
|
|
||||||
|
### `OnTakePhotoClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Captures photo via `MediaPicker`, saves to cache as `temp_form_capture.jpg`, updates preview, resets transforms.
|
||||||
|
|
||||||
|
### `OnPanUpdated(object sender, PanUpdatedEventArgs e)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Updates `TranslationX/Y` in real-time during `Running`; commits offsets to `_editX/_editY` on `Completed`.
|
||||||
|
|
||||||
|
### `OnFetchBadgeClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Generates and assigns unique badge to `Data9Entry` via `GenerateUniqueBadgeNumberAsync()`.
|
||||||
|
|
||||||
|
### `OnSaveClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Validates `Data2Entry`, copies temp photo to `_photosBasePath` with unique name, builds `SqlParameter` array, executes `INSERT`/`UPDATE`, invokes callback, sets `IsSaved = true`, and pops modal.
|
||||||
|
|
||||||
|
### `GenerateUniqueBadgeNumberAsync()`
|
||||||
|
- **Access:** `private async Task<string>`
|
||||||
|
- **Returns:** `Task<string>` (Format: `*$F{6-digit}$A*`)
|
||||||
|
|
||||||
|
### `GenerateNextRecordNumber()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Returns:** `string`
|
||||||
|
- **Behavior:** Queries `MAX(CAST(Data1 AS INT))`, returns next sequential number (starts at `100001`). *Currently unused in save flow.*
|
||||||
|
|
||||||
|
### `SafeToDouble(object? val, double defaultVal)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Returns:** `double`
|
||||||
|
- **Behavior:** Safely parses object to `double`, returns `defaultVal` on failure/null.
|
||||||
|
|
||||||
|
### `OnCancelClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Closes modal without saving.
|
||||||
|
|
||||||
|
---
|
||||||
|
**⚠️ Developer Notes:**
|
||||||
|
- `OnSaveClicked` contains a nested `try/catch` that calls `Navigation.PopModalAsync()` twice on success. Remove the outer call to prevent `InvalidOperationException`.
|
||||||
|
- `GenerateNextRecordNumber()` is implemented but never invoked. Wire it to auto-fill `Data1Entry` or remove it.
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
# EmployeePage
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `ContentPage`
|
||||||
|
**Purpose:** Primary employee management interface. Handles list filtering, dynamic badge preview generation, photo editing, database CRUD operations, and badge printing.
|
||||||
|
|
||||||
|
## 🔑 Fields & Collections
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `_db` | `SqlService` | `private readonly` | Database access service. |
|
||||||
|
| `_printerService` | `PrinterService` | `private readonly` | Printing service. |
|
||||||
|
| `_selectedEmployee` | `Dictionary<string, object>?` | `private` | Currently selected employee record. |
|
||||||
|
| `_currentCompany` | `Dictionary<string, object>` | `private readonly` | Company context passed via DI. |
|
||||||
|
| `_allEmployees` | `ObservableCollection<Dictionary<string, object>>` | `private` | Master list of employees. |
|
||||||
|
| `_filteredEmployees` | `ObservableCollection<Dictionary<string, object>>` | `private` | Filtered view for UI binding. |
|
||||||
|
| `_photosBasePath`, `_logosBasePath`, `_imagesBasePath` | `string` | `private readonly` | Storage paths from Preferences. |
|
||||||
|
| `_currentBadgeType` | `string` | `private` | Tracks active badge style (`OFFICE`, `GUEST`, `PLANT`, etc.) |
|
||||||
|
| `_showActiveOnly` | `bool` | `private` | Active-only filter state. |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `EmployeePage(SqlService db, PrinterService printerService, Dictionary<string, object> company)`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Behavior:** Initializes UI, resolves XAML controls via `FindByName`, sets up event handlers (`ActiveFilter`, `Search`, `Guest/Badge Pickers`), ensures directories exist, loads company logo, fetches employees, and displays placeholder message.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### UI & State Management
|
||||||
|
| Method | Access | Behavior |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| `ShowNoSelectionMessage()` | `private` | Renders placeholder `VerticalStackLayout` in `PreviewFrame`. |
|
||||||
|
| `ResetPageToDefault()` | `private` | Clears selection, search text, filtered list, and resets preview. Prevents filter triggers during reset. |
|
||||||
|
| `OnAppearing()` | `protected override async void` | Resets UI, loads printers, and refreshes employee data. |
|
||||||
|
| `LoadCompanyLogo()` | `private` | Resolves logo path via `GetCompanyLogoPath()`. |
|
||||||
|
| `GetCompanyLogoPath()` | `private` | Combines `_logosBasePath` with DB filename. Validates existence, logs warnings. |
|
||||||
|
|
||||||
|
### Data Loading & Filtering
|
||||||
|
| Method | Access | Behavior |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| `LoadEmployeesAsync()` | `private async Task` | Runs DB query on background thread. Marshals to main thread to populate `_allEmployees` and trigger filters. |
|
||||||
|
| `LoadEmployees()` | `private` | Synchronous variant (legacy). |
|
||||||
|
| `ApplyFilters()` | `private` | Filters `_allEmployees` by `_showActiveOnly` and search bar text. Updates `_filteredEmployees` on main thread. |
|
||||||
|
| `OnSearchTextChanged()` | `private` | Positions dropdown, shows list, triggers filters. |
|
||||||
|
| `PositionSearchResultsDropdown()` | `private` | Sets `TranslationY` and `ZIndex` for search results list. |
|
||||||
|
|
||||||
|
### Employee Selection & Navigation
|
||||||
|
| Method | Access | Behavior |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| `OnEmployeeSelected()` | `private` | Updates search bar, resolves badge type (case-insensitive), toggles guest selector, renders preview. |
|
||||||
|
| `SelectAndShowEmployee(Dictionary)` | `private` | Helper to find record in collection, update UI, and scroll `ListView` to item. |
|
||||||
|
| `OnAddEmployeeClicked()` | `private async void` | Opens `EmployeeFormPage` modally. Sets callback to reload list and auto-select new record. |
|
||||||
|
| `OnEditEmployeeClicked()` | `private async void` | Opens form in edit mode. Refreshes data and preview if `IsSaved` is true. |
|
||||||
|
|
||||||
|
### Badge Rendering & Photo Editing
|
||||||
|
| Method | Access | Behavior |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| `RenderBadgePreview(Dictionary)` | `private` | **Core method.** Dynamically builds front/back badge UI based on `_currentBadgeType`. Handles company logo, employee photo, barcode, medical indicators, and company details. |
|
||||||
|
| `AddStandardFront()` | `private` | UI builder for badge front (logo, photo container, name, barcode, medical icons). |
|
||||||
|
| `AddStandardBackDetails()` | `private` | UI builder for badge back (property text, address, ID, issue date, medical icons). |
|
||||||
|
| `OnTakePhotoClicked()` | `private async void` | Captures photo, saves to cache, resets editor transforms, shows overlay. |
|
||||||
|
| `OnPanUpdated()` | `private` | Updates photo `TranslationX/Y` during pan. Commits offsets on completion. |
|
||||||
|
| `OnApplyPhoto()` | `private async void` | Copies temp photo to `_photosBasePath`, updates DB (`picCode`, `cropScale`, `cropX`, `cropY`), refreshes preview. |
|
||||||
|
| `OnCancelPhoto()` | `private` | Hides photo editor overlay. |
|
||||||
|
| `InitialiseDefaultImages()` | `private async Task` | Deploys `Guest.png`/`WelBilt.png` from `Resources/Raw` to images directory if missing. |
|
||||||
|
|
||||||
|
### Utilities & Printing
|
||||||
|
| Method | Access | Behavior |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| `SafeToDouble(object?, double)` | `private` | Safe parsing helper. Returns `defaultValue` on null/parse failure. |
|
||||||
|
| `OnBadgeTypeChanged()` | `private` | Updates `_currentBadgeType`, saves to DB, refreshes preview. |
|
||||||
|
| `OnGuestImageChanged()` | `private` | Updates `_currentGuestImage`, refreshes preview if type is `GUEST`. |
|
||||||
|
| `LoadAvailablePrinters()` | `private` | Populates `PrinterPicker` from `PrinterSettings.InstalledPrinters`. |
|
||||||
|
| `OnRefreshPrintersClicked()` | `private` | Reloads printer list. |
|
||||||
|
| `OnPrintClicked()` | `private async void` | Validates selection/printer. Captures front/back views via `CaptureAsync()`, converts to Base64, sends to `_printerService`. |
|
||||||
|
| `StreamToBase64(IScreenshotResult)` | `private async Task` | Converts screenshot stream to Base64 string for printing. |
|
||||||
|
|
||||||
|
---
|
||||||
|
**💡 Architectural Notes:**
|
||||||
|
- **Dynamic UI Generation:** Badge preview is built entirely in C# (`HorizontalStackLayout`, `Frame`, `Grid`). This allows precise control over print dimensions but increases memory overhead.
|
||||||
|
- **Threading:** DB queries run on `Task.Run` or background threads. All UI updates use `MainThread.BeginInvokeOnMainThread` or `InvokeOnMainThreadAsync`.
|
||||||
|
- **Null Safety:** Heavy use of `?.` and `FindByName` checks indicates defensive coding against missing XAML `x:Name` mappings.
|
||||||
|
- **Printing Flow:** Uses `CommunityToolkit.Maui` screenshot API to capture rendered UI, bypassing native printing drivers for pixel-perfect badge output.
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# MainPage
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `ContentPage`
|
||||||
|
**Purpose:** Default content page stub. Currently acts as a placeholder entry point.
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `MainPage()`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Behavior:** Calls `InitializeComponent()` to load XAML-defined UI. No additional logic or service injection is present.
|
||||||
|
|
||||||
|
---
|
||||||
|
**💡 Developer Notes:**
|
||||||
|
- Likely superseded by `AppShell` routing. If unused, consider removing or converting to a diagnostic/splash page.
|
||||||
|
- Ensure `MainPage.xaml` exists and contains valid root element if this class is referenced.
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# MauiProgram
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Type:** `static class`
|
||||||
|
**Purpose:** MAUI application bootstrap. Handles dependency injection setup, configuration loading, font registration, and service factory creation.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `CreateMauiApp()`
|
||||||
|
- **Access:** `public static`
|
||||||
|
- **Returns:** `MauiApp`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Logs startup.
|
||||||
|
2. Initializes `MauiAppBuilder`, enables `CommunityToolkit`, and sets fonts (`OpenSans`, `BarcodeFont`).
|
||||||
|
3. Calls `LoadConfiguration(builder)`.
|
||||||
|
4. Registers services:
|
||||||
|
- `SqlService`: Factory-resolves connection string from `IConfiguration`. Falls back to hardcoded `Server=127.0.0.1...` if missing. Logs warning.
|
||||||
|
- `PrinterService`: Singleton.
|
||||||
|
- `AppShell`: Singleton.
|
||||||
|
- `EmployeePage`, `CompanyPage`, `SettingsPage`: Transient.
|
||||||
|
5. Enables debug logging in `DEBUG` builds.
|
||||||
|
6. Builds and returns `MauiApp`.
|
||||||
|
|
||||||
|
### `LoadConfiguration(MauiAppBuilder builder)`
|
||||||
|
- **Access:** `private static`
|
||||||
|
- **Behavior:** Configuration fallback chain:
|
||||||
|
1. **Primary:** Loads `appsettings.json` via `FileSystem.OpenAppPackageFileAsync()`. Parses and injects into builder.
|
||||||
|
2. **Fallback 1:** If file system fails, attempts `Assembly.GetManifestResourceStream("FrymasterBadgeApp.Resources.Raw.appsettings.json")`.
|
||||||
|
3. **Fallback 2:** If both fail, silently returns (triggers `SqlService` hardcoded fallback).
|
||||||
|
4. Logs success or warnings at each stage.
|
||||||
|
|
||||||
|
---
|
||||||
|
**💡 Developer Notes:**
|
||||||
|
- ⚠️ **Security Warning:** Hardcoded fallback connection string (`User Id=sa;Password=YourPassword123;`) should be removed or masked before production deployment.
|
||||||
|
- Configuration loading gracefully degrades, preventing app crashes on missing config files.
|
||||||
|
- Service lifetimes are appropriately scoped (Singleton for app-wide services, Transient for UI pages).
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
# SettingsPage
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `ContentPage`
|
||||||
|
**Purpose:** Configuration interface for managing file storage paths (Photos, Logos, Images) and global UI theme preferences.
|
||||||
|
|
||||||
|
## 🔑 Constants & Fields
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `PhotoPathKey` | `const string` | `private` | Preference key: `"PhotoBasePath"` |
|
||||||
|
| `LogoPathKey` | `const string` | `private` | Preference key: `"LogoBasePath"` |
|
||||||
|
| `ImagesPathKey` | `const string` | `private` | Preference key: `"ImagesBasePath"` |
|
||||||
|
| `ThemePrefKey` | `const string` | `private` | Preference key: `"AppTheme"` |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `SettingsPage()`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Calls `InitializeComponent()`.
|
||||||
|
2. Loads current paths and theme via helper methods.
|
||||||
|
3. Wires `ThemePicker.SelectedIndexChanged` to `ApplyThemeSelection()`.
|
||||||
|
4. Overrides default back button behavior to navigate `Shell.Current.GoToAsync("..")`.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `LoadCurrentSettings()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Reads photo, logo, and image paths from `Preferences.Default`. Falls back to `C:\FrymasterData\...` and populates corresponding UI entries.
|
||||||
|
|
||||||
|
### `LoadCurrentThemePreference()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Reads `"AppTheme"` preference. Maps to `Light`/`Dark`/`System`, updates `ThemePicker.SelectedIndex` and `Application.Current.UserAppTheme`.
|
||||||
|
|
||||||
|
### `ApplyThemeSelection()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Reads `ThemePicker.SelectedIndex`. Updates `UserAppTheme` and persists selection to `Preferences.Default`. Handles null picker defensively.
|
||||||
|
|
||||||
|
### `OnPickPhotoFolderClicked`, `OnPickLogoFolderClicked`, `OnPickImagesFolderClicked`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Wrappers that invoke `PickFolderAsync` with specific update callbacks and dialog titles.
|
||||||
|
|
||||||
|
### `PickFolderAsync(Action<FolderPickerResult> onSuccess, string title)`
|
||||||
|
- **Access:** `private async Task`
|
||||||
|
- **Parameters:**
|
||||||
|
- `onSuccess` (`Action<FolderPickerResult>`) – Callback on successful selection.
|
||||||
|
- `title` (`string`) – Picker dialog title.
|
||||||
|
- **Behavior:** Invokes `FolderPicker.Default.PickAsync()`. On success, executes callback and calls `VerifyDirectory()`. Catches and displays alerts on failure.
|
||||||
|
|
||||||
|
### `OnSaveClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Trims and validates all three paths. Alerts if any are empty.
|
||||||
|
2. Saves paths to `Preferences.Default`.
|
||||||
|
3. Updates `StatusLabel` with success/error message and color.
|
||||||
|
4. Shows `Toast` (non-critical, wrapped in try/catch).
|
||||||
|
5. **Intentionally omits auto-navigation** to prevent known Shell navigation crashes.
|
||||||
|
6. Logs extensively via `AppLogger`.
|
||||||
|
|
||||||
|
### `SafeCreateDirectory(string path)`
|
||||||
|
- **Access:** `private static`
|
||||||
|
- **Behavior:** *(Commented out in current code but present)* Creates directory if missing. Logs failures but suppresses crashes.
|
||||||
|
|
||||||
|
### `VerifyDirectory(string path)`
|
||||||
|
- **Access:** `private static`
|
||||||
|
- **Behavior:** Checks `Directory.Exists(path)`. Creates it if missing. Used immediately after folder selection.
|
||||||
|
|
||||||
|
---
|
||||||
|
**💡 Developer Notes:**
|
||||||
|
- Uses `CommunityToolkit.Maui.Alerts` and `Storage` for modern picker/toast functionality.
|
||||||
|
- Hardcoded fallback paths (`C:\FrymasterData\...`) should be platform-aware for iOS/macOS/Android.
|
||||||
|
- Explicitly avoids `Shell.Current.GoToAsync("..")` after save to maintain navigation stability.
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# CompanyPage
|
||||||
|
|
||||||
|
**Namespace:** `FrymasterBadgeApp`
|
||||||
|
**Base Class:** `ContentPage`
|
||||||
|
**Purpose:** CRUD interface for managing company records and uploading/previewing company logos.
|
||||||
|
|
||||||
|
## 🔑 Fields
|
||||||
|
| Name | Type | Access | Description |
|
||||||
|
|------|------|--------|-------------|
|
||||||
|
| `_db` | `SqlService` | `private readonly` | Database service. |
|
||||||
|
| `_selectedCompany` | `Dictionary<string, object>?` | `private` | Currently selected company from the list. |
|
||||||
|
| `_tempLogoPath` | `string` | `private` | Path to currently selected/cached logo image. |
|
||||||
|
| `LogoPathKey` | `const string` | `private` | Preference key for logo directory (`"LogoBasePath"`). |
|
||||||
|
|
||||||
|
## 🛠 Constructor
|
||||||
|
### `CompanyPage(SqlService db)`
|
||||||
|
- **Access:** `public`
|
||||||
|
- **Behavior:** Configures custom back button behavior, calls `LoadCompanies()`.
|
||||||
|
|
||||||
|
## 📦 Methods
|
||||||
|
|
||||||
|
### `GetLogoDirectory()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Returns:** `string`
|
||||||
|
- **Behavior:** Reads logo directory from Preferences. Defaults to `C:\FrymasterData\logos`.
|
||||||
|
|
||||||
|
### `LoadCompanies()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Executes `SELECT * FROM Companies ORDER BY Name ASC`, binds to `CompanyList`. Catches and alerts on DB failure.
|
||||||
|
|
||||||
|
### `OnCompanySelectionChanged(object sender, SelectionChangedEventArgs e)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Updates form fields and loads logo preview on main thread. Clears fields if selection is null.
|
||||||
|
|
||||||
|
### `GetValue(Dictionary<string, object> dict, string key)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Returns:** `string`
|
||||||
|
- **Behavior:** Case-insensitive dictionary lookup. Returns trimmed value or empty string if missing.
|
||||||
|
|
||||||
|
### `OnSelectLogoClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Opens file picker filtering for images. Stores `FullPath` in `_tempLogoPath` and updates preview.
|
||||||
|
|
||||||
|
### `OnSaveClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:**
|
||||||
|
1. Validates company name.
|
||||||
|
2. Ensures logo directory exists (creates if missing).
|
||||||
|
3. If logo selected: copies to directory with timestamped name on collision. Stores only filename.
|
||||||
|
4. Executes `INSERT` or `UPDATE` via `_db.Execute`.
|
||||||
|
5. Refreshes list and clears form.
|
||||||
|
|
||||||
|
### `OnAddNewClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Deselects list item and clears form fields.
|
||||||
|
|
||||||
|
### `ClearFields()`
|
||||||
|
- **Access:** `private`
|
||||||
|
- **Behavior:** Resets `_selectedCompany`, `_tempLogoPath`, all text fields, logo preview, and hides delete button.
|
||||||
|
|
||||||
|
### `OnDeleteClicked(object sender, EventArgs e)`
|
||||||
|
- **Access:** `private async void`
|
||||||
|
- **Behavior:** Shows confirmation dialog, executes `DELETE FROM Companies WHERE ID=@id`, refreshes list, clears fields. Catches and alerts on error.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Data Flow Notes:**
|
||||||
|
- Logo paths are stored as filenames only in the database to keep DB portable.
|
||||||
|
- File operations use `File.Copy` with overwrite protection (`timestamp` suffix).
|
||||||
|
- All UI updates after async operations are marshaled via `MainThread.BeginInvokeOnMainThread`.
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentPage
|
||||||
|
x:Class="FrymasterBadgeApp.EmployeeFormPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
Title="Employee Editor">
|
||||||
|
<ScrollView>
|
||||||
|
<VerticalStackLayout Padding="0" Spacing="0">
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Padding="20,30"
|
||||||
|
BackgroundColor="{AppThemeBinding Light={StaticResource Gray100}, Dark={StaticResource Tertiary}}"
|
||||||
|
ColumnDefinitions="*, Auto">
|
||||||
|
<VerticalStackLayout Grid.Column="0" Spacing="5">
|
||||||
|
<Label
|
||||||
|
x:Name="TitleLabel"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="28"
|
||||||
|
Text="Employee Details" />
|
||||||
|
<Label
|
||||||
|
FontSize="14"
|
||||||
|
Text="Manage badge information and photo"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
<Button
|
||||||
|
Grid.Column="1"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
Clicked="OnCancelClicked"
|
||||||
|
FontSize="20"
|
||||||
|
Text="X"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<VerticalStackLayout Padding="30" Spacing="25">
|
||||||
|
<Grid ColumnDefinitions="*, *" ColumnSpacing="40">
|
||||||
|
|
||||||
|
<VerticalStackLayout Grid.Column="0" Spacing="18">
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="5">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="Record Number"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Entry x:Name="Data1Entry" Placeholder="ID Number" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="5">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="12"
|
||||||
|
Text="Full Name"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
<Entry x:Name="Data2Entry" Placeholder="Required" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*, *" ColumnSpacing="15">
|
||||||
|
<VerticalStackLayout Grid.Column="0" Spacing="5">
|
||||||
|
<Label FontSize="12" Text="Preferred Name" />
|
||||||
|
<Entry x:Name="Data3Entry" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
<VerticalStackLayout Grid.Column="1" Spacing="5">
|
||||||
|
<Label FontSize="12" Text="Last Name" />
|
||||||
|
<Entry x:Name="LastNameEntry" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="5">
|
||||||
|
<Label FontSize="12" Text="Job Title" />
|
||||||
|
<Entry x:Name="Data4Entry" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*, *" ColumnSpacing="15">
|
||||||
|
<VerticalStackLayout Grid.Column="0" Spacing="5">
|
||||||
|
<Label FontSize="12" Text="Issue Date" />
|
||||||
|
<DatePicker x:Name="Data5DatePicker" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
<VerticalStackLayout Grid.Column="1" Spacing="5">
|
||||||
|
<Label FontSize="12" Text="Badge Barcode" />
|
||||||
|
<Grid ColumnDefinitions="*, Auto">
|
||||||
|
<Entry
|
||||||
|
x:Name="Data9Entry"
|
||||||
|
FontSize="11"
|
||||||
|
IsReadOnly="True" />
|
||||||
|
<Button
|
||||||
|
x:Name="BadgeFetchButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Padding="10,0"
|
||||||
|
BackgroundColor="{StaticResource Secondary}"
|
||||||
|
Clicked="OnFetchBadgeClicked"
|
||||||
|
Text="🔄" />
|
||||||
|
</Grid>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Frame Style="{StaticResource CardFrame}">
|
||||||
|
<HorizontalStackLayout HorizontalOptions="Center" Spacing="20">
|
||||||
|
<HorizontalStackLayout Spacing="5">
|
||||||
|
<CheckBox x:Name="MedicalCheckBox" Color="DodgerBlue" />
|
||||||
|
<Label Text="Medical" VerticalOptions="Center" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
<HorizontalStackLayout Spacing="5">
|
||||||
|
<CheckBox x:Name="EmsCheckBox" Color="Red" />
|
||||||
|
<Label Text="EMS" VerticalOptions="Center" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
<HorizontalStackLayout Spacing="5">
|
||||||
|
<CheckBox x:Name="ActiveCheckBox" Color="{StaticResource Primary}" />
|
||||||
|
<Label Text="Active" VerticalOptions="Center" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</Frame>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<VerticalStackLayout Grid.Column="1" HorizontalOptions="Center" Spacing="20">
|
||||||
|
<Frame
|
||||||
|
Padding="0"
|
||||||
|
BackgroundColor="Black"
|
||||||
|
BorderColor="{StaticResource Primary}"
|
||||||
|
CornerRadius="140"
|
||||||
|
HeightRequest="280"
|
||||||
|
IsClippedToBounds="True"
|
||||||
|
WidthRequest="280">
|
||||||
|
<Image x:Name="EditorPhotoPreview" Aspect="AspectFill">
|
||||||
|
<Image.GestureRecognizers>
|
||||||
|
<PanGestureRecognizer PanUpdated="OnPanUpdated" />
|
||||||
|
</Image.GestureRecognizers>
|
||||||
|
</Image>
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="10" WidthRequest="280">
|
||||||
|
<Label
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
Text="Zoom Control" />
|
||||||
|
<Slider
|
||||||
|
x:Name="ZoomSlider"
|
||||||
|
Maximum="4"
|
||||||
|
Minimum="1"
|
||||||
|
MinimumTrackColor="{StaticResource Primary}"
|
||||||
|
Value="1" />
|
||||||
|
<Button
|
||||||
|
BackgroundColor="{StaticResource Primary}"
|
||||||
|
Clicked="OnTakePhotoClicked"
|
||||||
|
CornerRadius="20"
|
||||||
|
HeightRequest="45"
|
||||||
|
Text="Take New Photo"
|
||||||
|
TextColor="White" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<BoxView Margin="0,10" HeightRequest="1" Color="{StaticResource Gray500}" />
|
||||||
|
|
||||||
|
<HorizontalStackLayout HorizontalOptions="End" Spacing="15">
|
||||||
|
<Button
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
Clicked="OnCancelClicked"
|
||||||
|
Text="Cancel"
|
||||||
|
TextColor="{DynamicResource AppTextColor}"
|
||||||
|
WidthRequest="100" />
|
||||||
|
<Button
|
||||||
|
x:Name="SaveButton"
|
||||||
|
BackgroundColor="{StaticResource Primary}"
|
||||||
|
Clicked="OnSaveClicked"
|
||||||
|
CornerRadius="5"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
Text="Save Changes"
|
||||||
|
TextColor="White"
|
||||||
|
WidthRequest="200" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</ContentPage>
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
using System.Text;
|
||||||
|
using FrymasterBadgeApp.Services;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace FrymasterBadgeApp;
|
||||||
|
|
||||||
|
public partial class EmployeeFormPage : ContentPage
|
||||||
|
{
|
||||||
|
private readonly SqlService _db;
|
||||||
|
private readonly Dictionary<string, object>? _existingEmployee;
|
||||||
|
public Action<Dictionary<string, object>> OnSavedCallback { get; set; }
|
||||||
|
private readonly bool _isEditMode;
|
||||||
|
private string _tempCapturePath = "";
|
||||||
|
private double _editX = 0;
|
||||||
|
private double _editY = 0;
|
||||||
|
private readonly string _photosBasePath;
|
||||||
|
|
||||||
|
public bool IsSaved { get; private set; } = false;
|
||||||
|
public Dictionary<string, object>? SavedEmployee { get; private set; }
|
||||||
|
|
||||||
|
public EmployeeFormPage(SqlService db, Dictionary<string, object>? employee = null)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_db = db;
|
||||||
|
_existingEmployee = employee;
|
||||||
|
_isEditMode = employee != null;
|
||||||
|
_photosBasePath = Preferences.Default.Get("PhotoBasePath", @"C:\FrymasterData\photos");
|
||||||
|
|
||||||
|
EditorPhotoPreview.SetBinding(
|
||||||
|
Image.ScaleProperty,
|
||||||
|
new Binding("Value", source: ZoomSlider)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_isEditMode)
|
||||||
|
SetupEditMode(employee!);
|
||||||
|
else
|
||||||
|
SetupAddMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupEditMode(Dictionary<string, object> employee)
|
||||||
|
{
|
||||||
|
TitleLabel.Text = "Edit Employee";
|
||||||
|
SaveButton.Text = "Update";
|
||||||
|
BadgeFetchButton.Text = "Update Badge";
|
||||||
|
|
||||||
|
Data1Entry.Text = employee.GetValueOrDefault("Data1")?.ToString() ?? "";
|
||||||
|
Data1Entry.IsEnabled = false;
|
||||||
|
Data2Entry.Text = employee.GetValueOrDefault("Data2")?.ToString() ?? "";
|
||||||
|
Data3Entry.Text = employee.GetValueOrDefault("Data3")?.ToString() ?? "";
|
||||||
|
LastNameEntry.Text = employee.GetValueOrDefault("LastName")?.ToString() ?? "";
|
||||||
|
Data4Entry.Text = employee.GetValueOrDefault("Data4")?.ToString() ?? "";
|
||||||
|
Data9Entry.Text = employee.GetValueOrDefault("Data9")?.ToString() ?? "";
|
||||||
|
|
||||||
|
if (
|
||||||
|
DateTime.TryParse(employee.GetValueOrDefault("Data5")?.ToString(), out DateTime dateVal)
|
||||||
|
)
|
||||||
|
Data5DatePicker.Date = dateVal;
|
||||||
|
|
||||||
|
MedicalCheckBox.IsChecked =
|
||||||
|
(employee.GetValueOrDefault("Data7")?.ToString() ?? "False") == "True";
|
||||||
|
EmsCheckBox.IsChecked = (employee.GetValueOrDefault("Data8")?.ToString() ?? "0") == "1";
|
||||||
|
ActiveCheckBox.IsChecked =
|
||||||
|
(employee.GetValueOrDefault("Active")?.ToString()?.ToUpper() ?? "NO") == "YES";
|
||||||
|
|
||||||
|
string picCode = employee.GetValueOrDefault("picCode")?.ToString() ?? "";
|
||||||
|
string photoPath = Path.Combine(_photosBasePath, $"{picCode}.jpg");
|
||||||
|
|
||||||
|
if (File.Exists(photoPath))
|
||||||
|
{
|
||||||
|
EditorPhotoPreview.Source = ImageSource.FromFile(photoPath);
|
||||||
|
_editX = SafeToDouble(employee.GetValueOrDefault("cropX"), 0.0);
|
||||||
|
_editY = SafeToDouble(employee.GetValueOrDefault("cropY"), 0.0);
|
||||||
|
EditorPhotoPreview.TranslationX = _editX;
|
||||||
|
EditorPhotoPreview.TranslationY = _editY;
|
||||||
|
ZoomSlider.Value = SafeToDouble(employee.GetValueOrDefault("cropScale"), 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupAddMode()
|
||||||
|
{
|
||||||
|
TitleLabel.Text = "Add Employee";
|
||||||
|
Data1Entry.Text = "";
|
||||||
|
Data5DatePicker.Date = DateTime.Today;
|
||||||
|
ActiveCheckBox.IsChecked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTakePhotoClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var photo = await MediaPicker.Default.CapturePhotoAsync();
|
||||||
|
if (photo == null)
|
||||||
|
return;
|
||||||
|
_tempCapturePath = Path.Combine(FileSystem.CacheDirectory, "temp_form_capture.jpg");
|
||||||
|
using (var source = await photo.OpenReadAsync())
|
||||||
|
using (var destination = File.OpenWrite(_tempCapturePath))
|
||||||
|
await source.CopyToAsync(destination);
|
||||||
|
|
||||||
|
EditorPhotoPreview.Source = ImageSource.FromFile(_tempCapturePath);
|
||||||
|
_editX = _editY = 0;
|
||||||
|
EditorPhotoPreview.TranslationX = 0;
|
||||||
|
EditorPhotoPreview.TranslationY = 0;
|
||||||
|
ZoomSlider.Value = 1;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await DisplayAlert("Error", ex.Message, "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.StatusType == GestureStatus.Running)
|
||||||
|
{
|
||||||
|
EditorPhotoPreview.TranslationX = _editX + e.TotalX;
|
||||||
|
EditorPhotoPreview.TranslationY = _editY + e.TotalY;
|
||||||
|
}
|
||||||
|
else if (e.StatusType == GestureStatus.Completed)
|
||||||
|
{
|
||||||
|
_editX = EditorPhotoPreview.TranslationX;
|
||||||
|
_editY = EditorPhotoPreview.TranslationY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnFetchBadgeClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Data9Entry.Text = await GenerateUniqueBadgeNumberAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSaveClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Data2Entry.Text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string recordNum = Data1Entry.Text.Trim();
|
||||||
|
string picCode = _existingEmployee?.GetValueOrDefault("picCode")?.ToString() ?? "";
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_tempCapturePath))
|
||||||
|
{
|
||||||
|
picCode = $"{recordNum}_{DateTime.Now.Ticks}";
|
||||||
|
File.Copy(_tempCapturePath, Path.Combine(_photosBasePath, $"{picCode}.jpg"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = new List<SqlParameter>
|
||||||
|
{
|
||||||
|
new SqlParameter("@Data1", recordNum),
|
||||||
|
new SqlParameter("@Data2", Data2Entry.Text),
|
||||||
|
new SqlParameter("@Data3", Data3Entry.Text ?? ""),
|
||||||
|
new SqlParameter("@LastName", LastNameEntry.Text ?? ""),
|
||||||
|
new SqlParameter("@Data4", Data4Entry.Text ?? ""),
|
||||||
|
new SqlParameter("@Data5", Data5DatePicker.Date),
|
||||||
|
new SqlParameter("@Data9", Data9Entry.Text ?? ""),
|
||||||
|
new SqlParameter("@Data7", MedicalCheckBox.IsChecked ? "True" : "False"),
|
||||||
|
new SqlParameter("@Data8", EmsCheckBox.IsChecked ? "1" : "0"),
|
||||||
|
new SqlParameter("@Active", ActiveCheckBox.IsChecked ? "YES" : "NO"),
|
||||||
|
new SqlParameter("@picCode", picCode),
|
||||||
|
new SqlParameter("@cropX", EditorPhotoPreview.TranslationX),
|
||||||
|
new SqlParameter("@cropY", EditorPhotoPreview.TranslationY),
|
||||||
|
new SqlParameter("@cropScale", ZoomSlider.Value),
|
||||||
|
};
|
||||||
|
|
||||||
|
string sql = _isEditMode
|
||||||
|
? "UPDATE dbo.tblData SET Data1=@Data1, Data2=@Data2, Data3=@Data3, LastName=@LastName, Data4=@Data4, Data5=@Data5, Data9=@Data9, Data7=@Data7, Data8=@Data8, Active=@Active, picCode=@picCode, cropX=@cropX, cropY=@cropY, cropScale=@cropScale WHERE Data1=@Data1"
|
||||||
|
: "INSERT INTO dbo.tblData (Data1, Data2, Data3, LastName, Data4, Data5, Data9, Data7, Data8, Active, picCode, cropX, cropY, cropScale, BadgeType, ToPrint, CdsPrinted) VALUES (@Data1, @Data2, @Data3, @LastName, @Data4, @Data5, @Data9, @Data7, @Data8, @Active, @picCode, @cropX, @cropY, @cropScale, 'Office', 0, 0)";
|
||||||
|
|
||||||
|
_db.Execute(sql, parameters.ToArray());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// After successful DB execution:
|
||||||
|
var savedData = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "Data1", recordNum },
|
||||||
|
{ "Data2", Data2Entry.Text },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger the callback before closing
|
||||||
|
OnSavedCallback?.Invoke(savedData);
|
||||||
|
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await DisplayAlert("Error", ex.Message, "OK");
|
||||||
|
}
|
||||||
|
IsSaved = true;
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await DisplayAlert("Save Error", ex.Message, "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateUniqueBadgeNumberAsync()
|
||||||
|
{
|
||||||
|
return $"*$F{Random.Shared.Next(100000, 999999)}$A*";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateNextRecordNumber()
|
||||||
|
{
|
||||||
|
var result = _db.Query(
|
||||||
|
"SELECT MAX(CAST(Data1 AS INT)) FROM dbo.tblData WHERE ISNUMERIC(Data1) = 1",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
.FirstOrDefault();
|
||||||
|
return (
|
||||||
|
result != null ? (int.Parse(result.Values.First().ToString()) + 1) : 100001
|
||||||
|
).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double SafeToDouble(object? val, double defaultVal) =>
|
||||||
|
val != null && double.TryParse(val.ToString(), out double res) ? res : defaultVal;
|
||||||
|
|
||||||
|
private async void OnCancelClicked(object sender, EventArgs e) =>
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
|
@ -1,241 +1,239 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<ContentPage
|
<ContentPage
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="FrymasterBadgeApp.EmployeePage"
|
x:Class="FrymasterBadgeApp.EmployeePage"
|
||||||
BackgroundColor="#0f172a"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
>
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
<Grid RowDefinitions="80,*" ColumnDefinitions="400,*">
|
<Grid RowDefinitions="Auto, *">
|
||||||
<!-- Header -->
|
<Frame
|
||||||
<Border Grid.ColumnSpan="2" BackgroundColor="#1e293b" StrokeThickness="0" Padding="20,0">
|
Grid.Row="0"
|
||||||
<Grid ColumnDefinitions="Auto,*">
|
Padding="10"
|
||||||
<Label
|
BackgroundColor="{AppThemeBinding Light={StaticResource BgLight},
|
||||||
Text="FRYMASTER BADGE SYSTEM"
|
Dark={StaticResource Tertiary}}"
|
||||||
FontSize="20"
|
BorderColor="Transparent"
|
||||||
FontAttributes="Bold"
|
CornerRadius="0"
|
||||||
TextColor="White"
|
HasShadow="False">
|
||||||
VerticalOptions="Center"
|
|
||||||
/>
|
<VerticalStackLayout HorizontalOptions="Start" Spacing="15">
|
||||||
|
<FlexLayout
|
||||||
|
AlignItems="Center"
|
||||||
|
Direction="Row"
|
||||||
|
JustifyContent="Start"
|
||||||
|
Wrap="Wrap">
|
||||||
|
|
||||||
|
<VerticalStackLayout Margin="5" WidthRequest="300">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
Grid.Column="1"
|
|
||||||
x:Name="EmployeeSearchBar"
|
x:Name="EmployeeSearchBar"
|
||||||
Placeholder="Search employees..."
|
|
||||||
TextColor="White"
|
|
||||||
BackgroundColor="#334155"
|
|
||||||
PlaceholderColor="#94a3b8"
|
|
||||||
Margin="10,0"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
TextChanged="OnSearchTextChanged"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Left: Employee List -->
|
|
||||||
<Border Grid.Row="1" BackgroundColor="#1e293b" StrokeThickness="0">
|
|
||||||
<CollectionView
|
|
||||||
x:Name="EmployeeList"
|
|
||||||
SelectionMode="Single"
|
|
||||||
SelectionChanged="OnSelectionChanged"
|
|
||||||
BackgroundColor="Transparent"
|
BackgroundColor="Transparent"
|
||||||
Margin="10"
|
HeightRequest="50"
|
||||||
>
|
Placeholder="Search Employee..."
|
||||||
<CollectionView.ItemsLayout>
|
PlaceholderColor="{StaticResource Gray500}"
|
||||||
<LinearItemsLayout Orientation="Vertical" ItemSpacing="8" />
|
TextChanged="OnSearchTextChanged"
|
||||||
</CollectionView.ItemsLayout>
|
TextColor="{StaticResource Gray500}" />
|
||||||
<CollectionView.ItemTemplate>
|
</VerticalStackLayout>
|
||||||
<DataTemplate>
|
|
||||||
<Border
|
|
||||||
BackgroundColor="#334155"
|
|
||||||
Padding="15"
|
|
||||||
StrokeShape="RoundRectangle 8"
|
|
||||||
Shadow="Shadow 0 4 8 0.2 Black"
|
|
||||||
>
|
|
||||||
<Label
|
|
||||||
Text="{Binding [Data2]}"
|
|
||||||
TextColor="White"
|
|
||||||
FontSize="16"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
/>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</CollectionView.ItemTemplate>
|
|
||||||
</CollectionView>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Right: Preview & Controls -->
|
<HorizontalStackLayout>
|
||||||
<ScrollView Grid.Row="1" Grid.Column="1" Padding="40,20">
|
<CheckBox
|
||||||
<VerticalStackLayout Spacing="25" HorizontalOptions="Center">
|
x:Name="ActiveFilterCheckBox"
|
||||||
<!-- Company Logo (used in badge preview) -->
|
Margin="0,0,10,0"
|
||||||
<Image
|
CheckedChanged="OnActiveFilterChanged"
|
||||||
x:Name="PreviewLogoFront"
|
IsChecked="True"
|
||||||
HeightRequest="60"
|
VerticalOptions="Center" />
|
||||||
Aspect="AspectFit"
|
|
||||||
HorizontalOptions="Center"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Badge Type & Guest Selector -->
|
|
||||||
<Border
|
|
||||||
BackgroundColor="#1e293b"
|
|
||||||
Padding="20"
|
|
||||||
StrokeShape="RoundRectangle 12"
|
|
||||||
Shadow="Shadow 0 4 10 0.3 Black"
|
|
||||||
>
|
|
||||||
<VerticalStackLayout Spacing="15">
|
|
||||||
<HorizontalStackLayout Spacing="15" HorizontalOptions="Center">
|
|
||||||
<Label
|
<Label
|
||||||
Text="Badge Type"
|
Margin="0,0,40,0"
|
||||||
TextColor="White"
|
Text="Active Only"
|
||||||
FontAttributes="Bold"
|
VerticalOptions="Center" />
|
||||||
FontSize="16"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
/>
|
|
||||||
<Picker
|
|
||||||
x:Name="BadgeTypePicker"
|
|
||||||
Title="Select type"
|
|
||||||
WidthRequest="250"
|
|
||||||
TextColor="White"
|
|
||||||
BackgroundColor="#334155"
|
|
||||||
SelectedIndexChanged="OnBadgeTypeChanged"
|
|
||||||
>
|
|
||||||
<Picker.Items>
|
|
||||||
<x:String>Office</x:String>
|
|
||||||
<x:String>Plant</x:String>
|
|
||||||
<x:String>Maintenance</x:String>
|
|
||||||
<x:String>Guest</x:String>
|
|
||||||
<x:String>Visitor</x:String>
|
|
||||||
<x:String>Vendor</x:String>
|
|
||||||
</Picker.Items>
|
|
||||||
</Picker>
|
|
||||||
</HorizontalStackLayout>
|
</HorizontalStackLayout>
|
||||||
|
|
||||||
<HorizontalStackLayout
|
<HorizontalStackLayout
|
||||||
x:Name="GuestImageSelector"
|
x:Name="GuestImageSelector"
|
||||||
|
Margin="5"
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Spacing="15"
|
Spacing="5">
|
||||||
HorizontalOptions="Center"
|
<Label Text="Guest Img:" VerticalOptions="Center" />
|
||||||
>
|
|
||||||
<Label
|
|
||||||
Text="Guest Image"
|
|
||||||
TextColor="White"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
FontSize="16"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
/>
|
|
||||||
<Picker
|
<Picker
|
||||||
x:Name="GuestImagePicker"
|
x:Name="GuestImagePicker"
|
||||||
Title="Select image"
|
|
||||||
WidthRequest="250"
|
|
||||||
TextColor="White"
|
|
||||||
BackgroundColor="#334155"
|
|
||||||
SelectedIndexChanged="OnGuestImageChanged"
|
SelectedIndexChanged="OnGuestImageChanged"
|
||||||
>
|
TextColor="{DynamicResource AppTextColor}"
|
||||||
|
WidthRequest="130">
|
||||||
<Picker.Items>
|
<Picker.Items>
|
||||||
<x:String>Guest1.png</x:String>
|
<x:String>Guest.jpg</x:String>
|
||||||
<x:String>Guest2.png</x:String>
|
<x:String>WelBilt.jpg</x:String>
|
||||||
<x:String>Guest3.png</x:String>
|
|
||||||
</Picker.Items>
|
</Picker.Items>
|
||||||
</Picker>
|
</Picker>
|
||||||
</HorizontalStackLayout>
|
</HorizontalStackLayout>
|
||||||
</VerticalStackLayout>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Badge Preview (Front + Back side-by-side) -->
|
<HorizontalStackLayout Margin="5" Spacing="5">
|
||||||
<Border
|
<Label Text="Badge Type:" VerticalOptions="Center" />
|
||||||
x:Name="PreviewFrame"
|
<Picker
|
||||||
BackgroundColor="Transparent"
|
x:Name="BadgeTypePicker"
|
||||||
Padding="30"
|
SelectedIndexChanged="OnBadgeTypeChanged"
|
||||||
StrokeShape="RoundRectangle 20"
|
TextColor="{StaticResource Gray500}"
|
||||||
Shadow="Shadow 0 8 20 0.4 Black"
|
WidthRequest="150">
|
||||||
HorizontalOptions="Center"
|
<Picker.Items>
|
||||||
>
|
<x:String>OFFICE</x:String>
|
||||||
<!-- Content dynamically set in code-behind -->
|
<x:String>PLANT</x:String>
|
||||||
</Border>
|
<x:String>MAINTENANCE</x:String>
|
||||||
|
<x:String>GUEST</x:String>
|
||||||
<!-- Action Buttons -->
|
<x:String>VENDOR</x:String>
|
||||||
<HorizontalStackLayout Spacing="20" HorizontalOptions="Center">
|
</Picker.Items>
|
||||||
<Button
|
</Picker>
|
||||||
Text="📷 Edit / Capture Photo"
|
|
||||||
Clicked="OnTakePhotoClicked"
|
|
||||||
BackgroundColor="#3b82f6"
|
|
||||||
TextColor="White"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
CornerRadius="12"
|
|
||||||
HeightRequest="55"
|
|
||||||
WidthRequest="220"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
Text="🖨️ Print Badge"
|
|
||||||
Clicked="OnPrintClicked"
|
|
||||||
BackgroundColor="#6366f1"
|
|
||||||
TextColor="White"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
CornerRadius="12"
|
|
||||||
HeightRequest="55"
|
|
||||||
WidthRequest="220"
|
|
||||||
/>
|
|
||||||
</HorizontalStackLayout>
|
</HorizontalStackLayout>
|
||||||
|
|
||||||
|
<HorizontalStackLayout Margin="5" Spacing="5">
|
||||||
|
<Label Text="Printer:" VerticalOptions="Center" />
|
||||||
|
<Picker
|
||||||
|
x:Name="PrinterPicker"
|
||||||
|
TextColor="{StaticResource Gray500}"
|
||||||
|
WidthRequest="180" />
|
||||||
|
<Button
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
Clicked="OnRefreshPrintersClicked"
|
||||||
|
Text="🔄"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</FlexLayout>
|
||||||
|
|
||||||
|
<FlexLayout
|
||||||
|
AlignItems="Center"
|
||||||
|
Direction="Row"
|
||||||
|
JustifyContent="Center"
|
||||||
|
Wrap="Wrap">
|
||||||
|
<Button
|
||||||
|
Margin="5"
|
||||||
|
Padding="20,0"
|
||||||
|
BackgroundColor="{StaticResource Success}"
|
||||||
|
Clicked="OnAddEmployeeClicked"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
HeightRequest="45"
|
||||||
|
Text="+ Add Employee"
|
||||||
|
TextColor="White" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
x:Name="EditSelectedButton"
|
||||||
|
Margin="5"
|
||||||
|
BackgroundColor="{StaticResource Info}"
|
||||||
|
Clicked="OnEditEmployeeClicked"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
HeightRequest="45"
|
||||||
|
Text="✎ Edit Selected"
|
||||||
|
TextColor="White"
|
||||||
|
WidthRequest="300" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Padding="20,0"
|
||||||
|
BackgroundColor="{StaticResource Primary}"
|
||||||
|
Clicked="OnPrintClicked"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
HeightRequest="45"
|
||||||
|
Text="PRINT BADGE"
|
||||||
|
TextColor="White" />
|
||||||
|
</FlexLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
<ScrollView Grid.Row="1" Padding="20">
|
||||||
|
<VerticalStackLayout HorizontalOptions="Center" Spacing="20">
|
||||||
|
<Frame
|
||||||
|
x:Name="PreviewFrame"
|
||||||
|
Padding="30"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
BorderColor="{AppThemeBinding Light={StaticResource BorderLight},
|
||||||
|
Dark={StaticResource BorderDark}}"
|
||||||
|
CornerRadius="20"
|
||||||
|
HasShadow="True"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
BackgroundColor="{StaticResource Info}"
|
||||||
|
Clicked="OnTakePhotoClicked"
|
||||||
|
CornerRadius="10"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
HeightRequest="50"
|
||||||
|
Text="📷 EDIT PHOTO"
|
||||||
|
TextColor="White"
|
||||||
|
WidthRequest="300" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Photo Editor Overlay -->
|
<ListView
|
||||||
<Grid x:Name="PhotoEditorOverlay" IsVisible="False" BackgroundColor="#CC000000">
|
x:Name="SearchResultsList"
|
||||||
<Border
|
Grid.RowSpan="2"
|
||||||
BackgroundColor="#1e293b"
|
Margin="0,0,20,0"
|
||||||
Padding="30"
|
BackgroundColor="{AppThemeBinding Light={StaticResource White},
|
||||||
StrokeShape="RoundRectangle 20"
|
Dark={StaticResource BgDark}}"
|
||||||
Shadow="Shadow 0 8 20 0.4 Black"
|
HeightRequest="250"
|
||||||
HorizontalOptions="Center"
|
HorizontalOptions="Start"
|
||||||
VerticalOptions="Center"
|
IsVisible="False"
|
||||||
WidthRequest="500"
|
ItemSelected="OnEmployeeSelected"
|
||||||
>
|
VerticalOptions="Start"
|
||||||
<VerticalStackLayout Spacing="25">
|
|
||||||
<Label
|
|
||||||
Text="Adjust Photo"
|
|
||||||
FontSize="20"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
TextColor="White"
|
|
||||||
HorizontalTextAlignment="Center"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Border
|
|
||||||
BackgroundColor="Black"
|
|
||||||
StrokeShape="RoundRectangle 12"
|
|
||||||
Padding="0"
|
|
||||||
IsClippedToBounds="True"
|
|
||||||
HeightRequest="300"
|
|
||||||
WidthRequest="300"
|
WidthRequest="300"
|
||||||
>
|
ZIndex="100">
|
||||||
<Image x:Name="EditorPhotoPreview" Aspect="AspectFill">
|
<ListView.ItemTemplate>
|
||||||
<Image.GestureRecognizers>
|
<DataTemplate>
|
||||||
<PanGestureRecognizer PanUpdated="OnPanUpdated" />
|
<ViewCell>
|
||||||
</Image.GestureRecognizers>
|
<VerticalStackLayout Padding="10">
|
||||||
</Image>
|
<Label
|
||||||
</Border>
|
FontAttributes="Bold"
|
||||||
|
Text="{Binding [Data2]}"
|
||||||
<VerticalStackLayout Spacing="8">
|
TextColor="{StaticResource Gray500}" />
|
||||||
<Label Text="Zoom" TextColor="#cbd5e1" HorizontalTextAlignment="Center" />
|
<Label
|
||||||
<Slider x:Name="ZoomSlider" Minimum="1" Maximum="4" Value="1" />
|
FontSize="12"
|
||||||
|
Text="{Binding [Data1]}"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
|
</ViewCell>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="20">
|
<Grid
|
||||||
|
x:Name="PhotoEditorOverlay"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
BackgroundColor="#80000000"
|
||||||
|
IsVisible="False">
|
||||||
|
<Border
|
||||||
|
Padding="25"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
Style="{StaticResource CardStyle}"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
WidthRequest="450">
|
||||||
|
<VerticalStackLayout Spacing="20">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="18"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
Text="PHOTO ADJUSTMENT" />
|
||||||
|
<Frame
|
||||||
|
Padding="0"
|
||||||
|
BackgroundColor="Black"
|
||||||
|
CornerRadius="10"
|
||||||
|
HeightRequest="300"
|
||||||
|
IsClippedToBounds="True"
|
||||||
|
WidthRequest="300">
|
||||||
|
<Image x:Name="EditorPhotoPreview" Aspect="AspectFill" />
|
||||||
|
</Frame>
|
||||||
|
<VerticalStackLayout Spacing="5">
|
||||||
|
<Label HorizontalOptions="Center" Text="Zoom Level" />
|
||||||
|
<Slider
|
||||||
|
x:Name="ZoomSlider"
|
||||||
|
Maximum="4"
|
||||||
|
Minimum="1"
|
||||||
|
Value="1" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
<Grid ColumnDefinitions="*, *" ColumnSpacing="15">
|
||||||
<Button
|
<Button
|
||||||
Text="Cancel"
|
BackgroundColor="{StaticResource Secondary}"
|
||||||
Clicked="OnCancelPhoto"
|
Clicked="OnCancelPhoto"
|
||||||
BackgroundColor="#ef4444"
|
Text="CANCEL"
|
||||||
TextColor="White"
|
TextColor="White" />
|
||||||
CornerRadius="10"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="1"
|
BackgroundColor="{StaticResource Success}"
|
||||||
Text="Save Crop"
|
|
||||||
Clicked="OnApplyPhoto"
|
Clicked="OnApplyPhoto"
|
||||||
BackgroundColor="#22c55e"
|
Text="SAVE CROP"
|
||||||
TextColor="White"
|
TextColor="White" />
|
||||||
CornerRadius="10"
|
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
|
||||||
1349
EmployeePage.xaml.cs
1349
EmployeePage.xaml.cs
File diff suppressed because it is too large
Load Diff
|
|
@ -1,85 +1,75 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net9.0-windows10.0.19041.0</TargetFrameworks>
|
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||||
<Platforms>x64</Platforms>
|
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<RootNamespace>FrymasterBadgeApp</RootNamespace>
|
|
||||||
<UseMaui>true</UseMaui>
|
<UseMaui>true</UseMaui>
|
||||||
<SingleProject>true</SingleProject>
|
<SingleProject>true</SingleProject>
|
||||||
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<EnableDefaultCssItems>false</EnableDefaultCssItems>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
<ApplicationTitle>FrymasterBadgeApp</ApplicationTitle>
|
|
||||||
<ApplicationId>com.c4b3r.frymasterbadgeapp</ApplicationId>
|
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
|
||||||
<ApplicationVersion>1</ApplicationVersion>
|
|
||||||
|
|
||||||
<WindowsPackageType>None</WindowsPackageType>
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||||
|
|
||||||
<SupportedOSPlatformVersion
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'"
|
<Platforms>x64</Platforms>
|
||||||
>10.0.17763.0</SupportedOSPlatformVersion
|
|
||||||
>
|
|
||||||
<TargetPlatformMinVersion
|
|
||||||
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'"
|
|
||||||
>10.0.17763.0</TargetPlatformMinVersion
|
|
||||||
>
|
|
||||||
|
|
||||||
<NoWarn>$(NoWarn);MSB3277;NU1605</NoWarn>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
|
<GenerateDefaultValueAttribute>false</GenerateDefaultValueAttribute>
|
||||||
|
<GenerateDocumentationFile>False</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
|
<Optimize>False</Optimize>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<MauiIcon
|
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
|
||||||
Include="Resources\AppIcon\appicon.svg"
|
|
||||||
ForegroundFile="Resources\AppIcon\appiconfg.svg"
|
|
||||||
Color="#512BD4"
|
|
||||||
/>
|
|
||||||
<MauiSplashScreen Include="Resources\Splash\splash.png" Color="#512BD4" BaseSize="128,128" />
|
|
||||||
<MauiImage Include="Resources\Images\*" />
|
|
||||||
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
|
|
||||||
<MauiFont Include="Resources\Fonts\*" />
|
|
||||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
|
||||||
<EmbeddedResource Include="Resources\Raw\appsettings.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0-windows10.0.19041.0'">
|
|
||||||
<MauiEnableXamlC>false</MauiEnableXamlC>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="Lextm.SharpSnmpLib" Version="12.5.6" />
|
|
||||||
<PackageReference Include="SkiaSharp" Version="3.116.1" />
|
|
||||||
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="3.116.1" />
|
|
||||||
<PackageReference Include="Neodynamic.SDK.BarcodeCore" Version="6.0.24.513" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="SdkApi.Card.Core">
|
<!-- Remove any old/fixed version pins if present -->
|
||||||
<HintPath>Libs\zebra\Sdk\SdkApi.Card.Core.dll</HintPath>
|
<!-- Update or add these with exact or minimum version -->
|
||||||
</Reference>
|
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.120" />
|
||||||
<Reference Include="SdkApi.Card.Desktop">
|
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.120" />
|
||||||
<HintPath>Libs\zebra\Sdk\SdkApi.Card.Desktop.dll</HintPath>
|
<!-- if you use it -->
|
||||||
</Reference>
|
<PackageReference Include="Microsoft.Maui.Core" Version="9.0.120" />
|
||||||
|
<!-- optional but good to align -->
|
||||||
|
|
||||||
|
<!-- Keep or add the toolkit -->
|
||||||
|
<PackageReference Include="CommunityToolkit.Maui" Version="12.3.0" />
|
||||||
|
|
||||||
|
<!-- Fix WindowsAppSDK if needed (usually follows MAUI) -->
|
||||||
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250909003" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<MauiAsset Include="Resources\Raw\appsettings.json" LogicalName="appsettings.json" />
|
||||||
|
<MauiFont Include="Resources\Fonts\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- <PackageReference Include="SdkApi.Core" Version="2.0.0" />
|
||||||
|
<PackageReference Include="SdkApi.Card.Core" Version="2.0.0" />
|
||||||
|
<PackageReference Include="SdkApi.Desktop" Version="2.0.0" />-->
|
||||||
<Reference Include="SdkApi.Core">
|
<Reference Include="SdkApi.Core">
|
||||||
<HintPath>Libs\zebra\Sdk\SdkApi.Core.dll</HintPath>
|
<HintPath>Libs\zebra\Sdk\SdkApi.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="SdkApi.Card.Core">
|
||||||
|
<HintPath>Libs\zebra\Sdk\SdkApi.Card.Core.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="SdkApi.Desktop">
|
<Reference Include="SdkApi.Desktop">
|
||||||
<HintPath>Libs\zebra\Sdk\SdkApi.Desktop.dll</HintPath>
|
<HintPath>Libs\zebra\Sdk\SdkApi.Desktop.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="SdkApi.Desktop.Usb">
|
<!--
|
||||||
<HintPath>Libs\zebra\Sdk\SdkApi.Desktop.Usb.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
|
|
||||||
<Content Include="Libs\zebra\Sdk\*.dll">
|
<Content Include="Libs\zebra\Sdk\*.dll">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
<Visible>false</Visible>
|
<Link>%(Filename)%(Extension)</Link>
|
||||||
</Content>
|
</Content> -->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<MauiXaml Update="Resources\Themes\DarkTheme.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</MauiXaml>
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- XAML files are compiled by the MAUI SDK implicitly; no explicit MauiXaml item includes to avoid duplicate resources -->
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
x:Class="FrymasterBadgeApp.MainPage"
|
x:Class="FrymasterBadgeApp.MainPage"
|
||||||
>
|
>
|
||||||
<VerticalStackLayout VerticalOptions="Center" HorizontalOptions="Center" Spacing="20">
|
<VerticalStackLayout VerticalOptions="Center" HorizontalOptions="Center" Spacing="20">
|
||||||
<Label Text="Frymaster Badge System" FontSize="24" FontAttributes="Bold" TextColor="#1F2937" />
|
<Label Text="Frymaster Badge System" FontSize="24" FontAttributes="Bold" TextColor="{DynamicResource AppTextColor}" />
|
||||||
<Label Text="Select a tab below to begin." TextColor="Gray" />
|
<Label Text="Select a tab below to begin." TextColor="{DynamicResource Gray500}" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
|
||||||
101
MauiProgram.cs
101
MauiProgram.cs
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using CommunityToolkit.Maui;
|
||||||
using FrymasterBadgeApp.Services;
|
using FrymasterBadgeApp.Services;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -9,45 +11,50 @@ public static class MauiProgram
|
||||||
{
|
{
|
||||||
public static MauiApp CreateMauiApp()
|
public static MauiApp CreateMauiApp()
|
||||||
{
|
{
|
||||||
|
AppLogger.Info("MauiProgram: Starting CreateMauiApp");
|
||||||
var builder = MauiApp.CreateBuilder();
|
var builder = MauiApp.CreateBuilder();
|
||||||
|
|
||||||
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
builder
|
builder
|
||||||
|
.UseMauiCommunityToolkit()
|
||||||
.UseMauiApp<App>()
|
.UseMauiApp<App>()
|
||||||
.ConfigureFonts(fonts =>
|
.ConfigureFonts(fonts =>
|
||||||
{
|
{
|
||||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||||
fonts.AddFont("LibreBarcode128-Regular.ttf", "BarcodeFont");
|
fonts.AddFont("LibreBarcode39-Regular.ttf", "BarcodeFont");
|
||||||
|
});
|
||||||
|
#pragma warning restore CA1416 // Validate platform compatibility
|
||||||
|
|
||||||
|
// 1. Load Configuration
|
||||||
|
LoadConfiguration(builder);
|
||||||
|
|
||||||
|
// 2. Register Services with Factory
|
||||||
|
builder.Services.AddSingleton<SqlService>(sp =>
|
||||||
|
{
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
var connectionString = config.GetConnectionString("DefaultConnection");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connectionString))
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
"SqlService: Connection string missing from config. Using HARDCODED FALLBACK."
|
||||||
|
);
|
||||||
|
// TODO: Replace with your actual SQL Server details
|
||||||
|
connectionString =
|
||||||
|
"Server=127.0.0.1;Database=Frymaster;User Id=sa;Password=YourPassword123;TrustServerCertificate=True;";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SqlService(connectionString);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Simplified Configuration Loading ---
|
|
||||||
// Instead of GetManifestResourceStream, we use the simpler FileSystem approach
|
|
||||||
// if the file is marked as a MauiAsset.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var configPath = "appsettings.json";
|
|
||||||
// Note: MauiAssets in Resources/Raw are accessed at the root of the app package
|
|
||||||
using var stream = FileSystem.OpenAppPackageFileAsync(configPath).Result;
|
|
||||||
if (stream != null)
|
|
||||||
{
|
|
||||||
var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
|
|
||||||
builder.Configuration.AddConfiguration(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine($"CONFIG WARNING: {ex.Message}");
|
|
||||||
// Don't 'throw' here unless it's truly fatal,
|
|
||||||
// otherwise the app won't even start.
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Register Services
|
|
||||||
builder.Services.AddSingleton<SqlService>();
|
|
||||||
builder.Services.AddSingleton<PrinterService>();
|
builder.Services.AddSingleton<PrinterService>();
|
||||||
|
|
||||||
// 2. Register Pages (Crucial for constructor injection)
|
// 3. Register Shell and Pages
|
||||||
builder.Services.AddTransient<MainPage>();
|
builder.Services.AddSingleton<AppShell>();
|
||||||
builder.Services.AddTransient<CompanyPage>();
|
|
||||||
builder.Services.AddTransient<EmployeePage>();
|
builder.Services.AddTransient<EmployeePage>();
|
||||||
|
builder.Services.AddTransient<CompanyPage>();
|
||||||
|
builder.Services.AddTransient<SettingsPage>();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
builder.Logging.AddDebug();
|
builder.Logging.AddDebug();
|
||||||
|
|
@ -55,4 +62,44 @@ public static class MauiProgram
|
||||||
|
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void LoadConfiguration(MauiAppBuilder builder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Standard MAUI way to read Resources\Raw files
|
||||||
|
using var stream = FileSystem.OpenAppPackageFileAsync("appsettings.json").Result;
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json)))
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
builder.Configuration.AddConfiguration(config);
|
||||||
|
AppLogger.Info("Config: appsettings.json loaded via FileSystem.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
$"Config: FileSystem load failed ({ex.Message}). Trying Manifest fallback..."
|
||||||
|
);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
string resourcePath = "FrymasterBadgeApp.Resources.Raw.appsettings.json";
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourcePath);
|
||||||
|
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
|
||||||
|
builder.Configuration.AddConfiguration(config);
|
||||||
|
AppLogger.Info("Config: Loaded via Manifest fallback.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ /* Final fallback to hardcoded string in SqlService factory */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
# Frymaster Badge App
|
||||||
|
|
||||||
|
A cross-platform **.NET MAUI** application for managing employee records, generating customizable ID badges, handling company profiles, and printing via local desktop printers. Features dynamic tab navigation, photo capture/cropping, barcode generation, and robust file-based logging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
| Requirement | Details |
|
||||||
|
|-------------|---------|
|
||||||
|
| **SDK** | `.NET 8.0` or later |
|
||||||
|
| **Workload** | `maui` workload installed (`dotnet workload install maui`) |
|
||||||
|
| **IDE** | Visual Studio 2022 (v17.8+) with MAUI, or VS Code + C# Dev Kit |
|
||||||
|
| **Database** | SQL Server (2019/2022/Express/Azure SQL) |
|
||||||
|
| **Platform** | Windows 10/11 (recommended for printing & file pickers) |
|
||||||
|
|
||||||
|
> ⚠️ **Note:** Printer APIs (`System.Drawing.Printing`) and folder/file pickers used in this app are desktop-optimized. Mobile targets may require platform-specific fallbacks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Database Setup
|
||||||
|
|
||||||
|
1. Create a database named `Frymaster` in your SQL Server instance.
|
||||||
|
2. Run the following schema to create the required tables (matches application queries):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE dbo.Companies (
|
||||||
|
ID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
Name NVARCHAR(100) NOT NULL,
|
||||||
|
Address NVARCHAR(200),
|
||||||
|
City NVARCHAR(100),
|
||||||
|
State NVARCHAR(50),
|
||||||
|
Zip NVARCHAR(20),
|
||||||
|
Logo NVARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE dbo.tblData (
|
||||||
|
Data1 NVARCHAR(50) PRIMARY KEY, -- Employee Record Number
|
||||||
|
Data2 NVARCHAR(100), -- First Name
|
||||||
|
LastName NVARCHAR(100),
|
||||||
|
Data3 NVARCHAR(100), -- Display Name
|
||||||
|
Data4 NVARCHAR(100), -- Additional Info
|
||||||
|
Data5 DATETIME, -- Issue/Start Date
|
||||||
|
Data7 NVARCHAR(10), -- Medical Indicator 1 (True/False)
|
||||||
|
Data8 NVARCHAR(10), -- Medical Indicator 2 (1/0 or True/False)
|
||||||
|
Active NVARCHAR(10), -- YES/NO
|
||||||
|
Data9 NVARCHAR(50), -- Badge Number
|
||||||
|
picCode NVARCHAR(255), -- Photo filename reference
|
||||||
|
cropX FLOAT DEFAULT 0,
|
||||||
|
cropY FLOAT DEFAULT 0,
|
||||||
|
cropScale FLOAT DEFAULT 1.0,
|
||||||
|
BadgeType NVARCHAR(50) DEFAULT 'OFFICE', -- OFFICE, GUEST, PLANT, MAINTENANCE, VENDOR
|
||||||
|
ToPrint BIT DEFAULT 0,
|
||||||
|
CdsPrinted BIT DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
1. Create `appsettings.json` inside `Resources/Raw/`
|
||||||
|
2. Set its **Build Action** to `MauiAsset`
|
||||||
|
3. Add your connection string:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=127.0.0.1;Database=Frymaster;User Id=sa;Password=YourSecurePassword;TrustServerCertificate=True;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 🔒 **Security Note:** The app contains a hardcoded fallback connection string in `MauiProgram.cs`. **Never deploy to production without providing `appsettings.json`**, as the fallback uses plain-text credentials and will log a warning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Building & Running
|
||||||
|
|
||||||
|
### ✅ Using CLI
|
||||||
|
```bash
|
||||||
|
# Restore dependencies
|
||||||
|
dotnet restore
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dotnet build -c Release
|
||||||
|
|
||||||
|
# Run (Windows Desktop default)
|
||||||
|
dotnet run -f net8.0-windows10.0.19041.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Using Visual Studio
|
||||||
|
1. Open `FrymasterBadgeApp.csproj` or `.sln`
|
||||||
|
2. Select your target framework in the toolbar (e.g., `net8.0-windows10.0.19041.0`)
|
||||||
|
3. Press `F5` or click ▶ Run
|
||||||
|
4. The app will automatically restore NuGet packages and launch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 First Run & Navigation Flow
|
||||||
|
|
||||||
|
1. **Initial Setup Tab**: If no companies exist in the database, the app shows a `Setup` tab. Click it to add your first company via `CompanyPage`.
|
||||||
|
2. **Dynamic Tabs**: Once companies are added, each gets its own tab in the bottom `TabBar`. Tabs are generated dynamically at runtime.
|
||||||
|
3. **Employee Management**: Click a company tab → select/search employees → view/edit badge preview → capture/update photos → print.
|
||||||
|
4. **Settings**: Use the top-right `⚙️` icon to configure storage paths (`C:\FrymasterData\...` defaults) and UI theme (`System`/`Light`/`Dark`).
|
||||||
|
5. **Company Management**: Use the `🏢` icon in `AppShell` to manage company records and upload logos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Storage & Paths
|
||||||
|
|
||||||
|
| Setting | Default | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| Photos | `C:\FrymasterData\photos` | Stores employee JPGs (`{recordNum}_{ticks}.jpg`) |
|
||||||
|
| Logos | `C:\FrymasterData\logos` | Stores company logo images |
|
||||||
|
| Images | `C:\FrymasterData\images` | Stores guest/default badge assets |
|
||||||
|
| Logs | `%LOCALAPPDATA%` | Rotates daily, max 5MB/file, 30-day auto-cleanup |
|
||||||
|
|
||||||
|
> 🌍 **Cross-Platform Note:** The `C:\...` defaults are Windows-specific. On macOS/Linux, change paths immediately via the **Settings Page** to avoid `UnauthorizedAccessException` or missing directory errors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| `SqlService` connection fails | Verify SQL Server is running, check `appsettings.json`, ensure `TrustServerCertificate=True` |
|
||||||
|
| App crashes on startup | Check `FrymasterBadgeApp_*.log` in `%LOCALAPPDATA%` for DI or XAML errors |
|
||||||
|
| Missing tabs / "Loading..." stuck | Ensure at least one company exists in `dbo.Companies` |
|
||||||
|
| Print fails / `PrinterSettings` empty | Ensure desktop target is selected. Mobile platforms don't expose `InstalledPrinters` |
|
||||||
|
| `FolderPicker` or `Toast` crashes | Wrapped in try/catch. Intentionally disables auto-navigation after save to prevent Shell crashes |
|
||||||
|
| Missing barcode font | Ensure `LibreBarcode39-Regular.ttf` is in `Resources/Fonts` with `Build Action: MauiFont` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 Logging & Diagnostics
|
||||||
|
|
||||||
|
- **Location:** `%LOCALAPPDATA%\FrymasterBadgeApp_YYYY-MM-DD.log`
|
||||||
|
- **Rotation:** Files >5MB are renamed to `.old` (previous `.old` is deleted)
|
||||||
|
- **Cleanup:** `AppLogger.CleanupOldLogs()` deletes logs older than 30 days
|
||||||
|
- **Viewing:** Open with any text editor or run:
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem $env:LOCALAPPDATA\FrymasterBadgeApp_*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Invoke-Item
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/your-feature`)
|
||||||
|
3. Commit changes (`git commit -m "Add your feature"`)
|
||||||
|
4. Push to the branch (`git push origin feature/your-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Property of Frymaster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 💡 **Tip:** Run `dotnet workload restore` before first build if you encounter missing MAUI SDK errors. Ensure your `.csproj` targets a supported Windows SDK version (`net8.0-windows10.0.19041.0` or higher).
|
||||||
|
|
@ -1,4 +1,113 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
|
width="456"
|
||||||
|
height="456"
|
||||||
|
viewBox="0 0 456 456"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="appicon.svg"
|
||||||
|
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect9"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.3"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false"
|
||||||
|
uniform="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect8"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.3"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false"
|
||||||
|
uniform="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.3"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false"
|
||||||
|
uniform="false" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.6951754"
|
||||||
|
inkscape:cx="227.70505"
|
||||||
|
inkscape:cy="228"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:13.9;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect2"
|
||||||
|
width="307.3428"
|
||||||
|
height="185.23157"
|
||||||
|
x="72.558861"
|
||||||
|
y="109.72316" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:13.9;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect3"
|
||||||
|
width="81.407501"
|
||||||
|
height="73.738678"
|
||||||
|
x="225.3454"
|
||||||
|
y="198.20956" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:13.9;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path3"
|
||||||
|
cx="265.45923"
|
||||||
|
cy="161.34023"
|
||||||
|
rx="23.006468"
|
||||||
|
ry="21.531694" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:13.9;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 75.508409,176.38292 c 0,0 102.054331,-1.76972 102.054331,-1.76972"
|
||||||
|
id="path7"
|
||||||
|
inkscape:path-effect="#path-effect7"
|
||||||
|
inkscape:original-d="M 75.508409,176.38292 177.56274,174.6132"
|
||||||
|
transform="translate(23.006469,53.68176)" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:13.9;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 80.227684,140.98836 c 0,0 81.407506,-0.58991 81.407506,-0.58991"
|
||||||
|
id="path8"
|
||||||
|
inkscape:path-effect="#path-effect8"
|
||||||
|
inkscape:original-d="m 80.227684,140.98836 81.407506,-0.58991"
|
||||||
|
transform="translate(23.006469,53.68176)" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:13.9;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 80.227684,106.77361 c 0,0 77.868046,-1.76973 77.868046,-1.76973"
|
||||||
|
id="path9"
|
||||||
|
inkscape:path-effect="#path-effect9"
|
||||||
|
inkscape:original-d="m 80.227684,106.77361 77.868046,-1.76973"
|
||||||
|
transform="translate(23.006469,53.68176)" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": "Server=ZABEASTPORTABLE\\SQLEXPRESS01;Database=FrymasterBadges;Trusted_Connection=True;TrustServerCertificate=True;"
|
"DefaultConnection": "Data Source=FRYDB003V\\FRYSQL1;Initial Catalog=HRBadge;Integrated Security=True;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;Application Name='Badge Printer';Command Timeout=0"
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=ZABEASTPORTABLE\\SQLEXPRESS01;Database=FrymasterBadges;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,33 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<?xaml-comp compile="true" ?>
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
<ResourceDictionary
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
>
|
|
||||||
<Color x:Key="Primary">#2563EB</Color>
|
|
||||||
<Color x:Key="PrimaryDark">#1E40AF</Color>
|
|
||||||
<Color x:Key="Secondary">#64748B</Color>
|
|
||||||
<Color x:Key="Tertiary">#F1F5F9</Color>
|
|
||||||
|
|
||||||
|
<Color x:Key="Primary">#2563EB</Color>
|
||||||
|
<Color x:Key="Secondary">#64748B</Color>
|
||||||
<Color x:Key="Success">#10B981</Color>
|
<Color x:Key="Success">#10B981</Color>
|
||||||
<Color x:Key="Danger">#EF4444</Color>
|
<Color x:Key="Danger">#EF4444</Color>
|
||||||
<Color x:Key="Warning">#F59E0B</Color>
|
<Color x:Key="Info">#3ABFF8</Color>
|
||||||
<Color x:Key="Info">#3B82F6</Color>
|
|
||||||
<Color x:Key="PageBgLight">#F9FAFB</Color>
|
|
||||||
<Color x:Key="PageBgDark">#111827</Color>
|
|
||||||
<Color x:Key="Black">#000000</Color>
|
|
||||||
<Color x:Key="White">#FFFFFF</Color>
|
<Color x:Key="White">#FFFFFF</Color>
|
||||||
|
<Color x:Key="Black">#000000</Color>
|
||||||
<Color x:Key="Gray100">#F3F4F6</Color>
|
<Color x:Key="Gray100">#F3F4F6</Color>
|
||||||
<Color x:Key="Gray200">#E5E7EB</Color>
|
|
||||||
<Color x:Key="Gray500">#6B7280</Color>
|
<Color x:Key="Gray500">#6B7280</Color>
|
||||||
<Color x:Key="Gray900">#111827</Color>
|
<Color x:Key="Tertiary">#2D3748</Color>
|
||||||
|
|
||||||
|
<Color x:Key="BgLight">#F9FAFB</Color>
|
||||||
|
<Color x:Key="TextLight">#111827</Color>
|
||||||
|
<Color x:Key="CardLight">#FFFFFF</Color>
|
||||||
|
<Color x:Key="BorderLight">#E5E7EB</Color>
|
||||||
|
<Color x:Key="EntryLight">#F0F0F0</Color>
|
||||||
|
|
||||||
|
<Color x:Key="BgDark">#0F172A</Color>
|
||||||
|
<Color x:Key="TextDark">#FFFFFF</Color>
|
||||||
|
<Color x:Key="CardDark">#111827</Color>
|
||||||
|
<Color x:Key="BorderDark">#374151</Color>
|
||||||
|
<Color x:Key="EntryDark">#1F2937</Color>
|
||||||
|
|
||||||
|
<Color x:Key="PageBg">#F9FAFB</Color>
|
||||||
|
<Color x:Key="CardBackground">#FFFFFF</Color>
|
||||||
|
<Color x:Key="CardBorder">#E5E7EB</Color>
|
||||||
|
<Color x:Key="AppTextColor">#111827</Color>
|
||||||
|
|
||||||
<Color x:Key="AppBackgroundColor">#FFFFFF</Color>
|
|
||||||
<Color x:Key="NavigationPrimary">#2563EB</Color>
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<?xaml-comp compile="true" ?>
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
<ResourceDictionary
|
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<Style ApplyToDerivedTypes="True" TargetType="ContentPage">
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource BgLight}, Dark={StaticResource BgDark}}" />
|
||||||
>
|
</Style>
|
||||||
<Style TargetType="Label">
|
|
||||||
<Setter Property="TextColor" Value="{StaticResource Gray900}" />
|
<Style ApplyToDerivedTypes="True" TargetType="Label">
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextLight}, Dark={StaticResource TextDark}}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Entry">
|
||||||
|
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextLight}, Dark={StaticResource TextDark}}" />
|
||||||
|
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource EntryLight}, Dark={StaticResource EntryDark}}" />
|
||||||
|
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CardStyle" TargetType="Border">
|
||||||
|
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource CardLight}, Dark={StaticResource CardDark}}" />
|
||||||
|
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource BorderLight}, Dark={StaticResource BorderDark}}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
|
<Setter Property="StrokeShape" Value="RoundRectangle 10" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="CardFrame" TargetType="Frame">
|
<Style x:Key="CardFrame" TargetType="Frame">
|
||||||
<Setter Property="BackgroundColor" Value="{StaticResource White}" />
|
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource CardLight}, Dark={StaticResource CardDark}}" />
|
||||||
<Setter Property="BorderColor" Value="{StaticResource Gray200}" />
|
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource BorderLight}, Dark={StaticResource BorderDark}}" />
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
|
||||||
<Setter Property="HasShadow" Value="True" />
|
<Setter Property="HasShadow" Value="True" />
|
||||||
<Setter Property="Padding" Value="15" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="EntryStyle" TargetType="Entry">
|
|
||||||
<Setter Property="TextColor" Value="{StaticResource Gray900}" />
|
|
||||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
|
||||||
<Setter Property="FontSize" Value="14" />
|
|
||||||
<Setter Property="BackgroundColor" Value="{StaticResource Gray100}" />
|
|
||||||
<Setter Property="Margin" Value="0,5" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="NavigationPage">
|
|
||||||
<Setter Property="BarBackgroundColor" Value="{StaticResource Primary}" />
|
|
||||||
<Setter Property="BarTextColor" Value="{StaticResource White}" />
|
|
||||||
</Style>
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
|
|
@ -1,234 +1,107 @@
|
||||||
using System.Text;
|
using System.Drawing;
|
||||||
using SkiaSharp;
|
// We move the usings outside the IF for the Language Server,
|
||||||
using Zebra.Sdk.Card.Containers;
|
// but keep the SupportedOSPlatform attribute to satisfy the linker.
|
||||||
using Zebra.Sdk.Card.Enumerations;
|
using System.Drawing.Printing;
|
||||||
using Zebra.Sdk.Card.Graphics;
|
using System.IO;
|
||||||
using Zebra.Sdk.Card.Printer;
|
using System.Runtime.Versioning;
|
||||||
using Zebra.Sdk.Comm;
|
|
||||||
using Zebra.Sdk.Printer.Discovery;
|
[assembly: SupportedOSPlatform("windows")]
|
||||||
|
|
||||||
namespace FrymasterBadgeApp.Services;
|
namespace FrymasterBadgeApp.Services;
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
public class PrinterService
|
public class PrinterService
|
||||||
{
|
{
|
||||||
public async Task<bool> PrintBadge(
|
public PrinterService()
|
||||||
Dictionary<string, object> emp,
|
|
||||||
Dictionary<string, object> comp,
|
|
||||||
string ip
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
|
AppLogger.Info("PrinterService: Constructor started.");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var frontBitmap = GenerateFrontSide(emp, comp);
|
var test = System.Drawing.Color.White;
|
||||||
using var backBitmap = GenerateBackSide(emp, comp);
|
AppLogger.Info("PrinterService: GDI+ Compatibility check passed.");
|
||||||
|
|
||||||
string frontBase64 = ConvertBitmapToBase64(frontBitmap);
|
|
||||||
string backBase64 = ConvertBitmapToBase64(backBitmap);
|
|
||||||
|
|
||||||
var (ok, error) = await PrintBadgeImages(frontBase64, backBase64, ip);
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"Print Engine Error: {ex.Message}");
|
AppLogger.Error("PrinterService: GDI+ Compatibility check FAILED.", ex);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SKBitmap GenerateFrontSide(
|
public void PrintBase64Badge(string frontBase64, string backBase64, string printerName)
|
||||||
Dictionary<string, object> emp,
|
|
||||||
Dictionary<string, object> comp
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var bitmap = new SKBitmap(648, 1016);
|
// Wrap the actual logic in the WINDOWS check so it doesn't try to compile on other platforms
|
||||||
using var canvas = new SKCanvas(bitmap);
|
#if WINDOWS
|
||||||
canvas.Clear(SKColors.White);
|
|
||||||
|
|
||||||
using var paint = new SKPaint { IsAntialias = true };
|
|
||||||
using var font = new SKFont(SKTypeface.FromFamilyName("Arial"), 40);
|
|
||||||
|
|
||||||
// 1. Draw Logo
|
|
||||||
string logoPath = comp.InternalSafeGet("logo");
|
|
||||||
if (File.Exists(logoPath))
|
|
||||||
{
|
|
||||||
using var logo = SKBitmap.Decode(logoPath);
|
|
||||||
canvas.DrawBitmap(logo, SKRect.Create(224, 40, 200, 80));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Draw Photo
|
|
||||||
string photoPath = emp.InternalSafeGet("ImagePath");
|
|
||||||
if (File.Exists(photoPath))
|
|
||||||
{
|
|
||||||
using var photo = SKBitmap.Decode(photoPath);
|
|
||||||
canvas.DrawBitmap(photo, SKRect.Create(149, 160, 350, 350));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Draw First Name (Red)
|
|
||||||
paint.Color = SKColors.Red;
|
|
||||||
font.Size = 70;
|
|
||||||
font.Embolden = true;
|
|
||||||
string firstName = emp.InternalSafeGet("Data2").Split(' ')[0].ToUpper();
|
|
||||||
canvas.DrawText(firstName, 324, 600, SKTextAlign.Center, font, paint);
|
|
||||||
|
|
||||||
// 4. Draw Full Name
|
|
||||||
paint.Color = SKColors.Black;
|
|
||||||
font.Size = 40;
|
|
||||||
font.Embolden = false;
|
|
||||||
canvas.DrawText(emp.InternalSafeGet("Data2"), 324, 660, SKTextAlign.Center, font, paint);
|
|
||||||
|
|
||||||
// 5. Draw Barcode Text
|
|
||||||
font.Size = 80;
|
|
||||||
canvas.DrawText(
|
|
||||||
$"*{emp.InternalSafeGet("Data9")}*",
|
|
||||||
324,
|
|
||||||
850,
|
|
||||||
SKTextAlign.Center,
|
|
||||||
font,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SKBitmap GenerateBackSide(
|
|
||||||
Dictionary<string, object> emp,
|
|
||||||
Dictionary<string, object> comp
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var bitmap = new SKBitmap(1016, 648);
|
|
||||||
using var canvas = new SKCanvas(bitmap);
|
|
||||||
canvas.Clear(SKColors.White);
|
|
||||||
|
|
||||||
using var paint = new SKPaint { IsAntialias = true, Color = SKColors.Black };
|
|
||||||
using var font = new SKFont(SKTypeface.FromFamilyName("Arial"), 25);
|
|
||||||
|
|
||||||
canvas.DrawText(
|
|
||||||
$"This badge is the property of {comp.InternalSafeGet("Name")} L.L.C.",
|
|
||||||
508,
|
|
||||||
100,
|
|
||||||
SKTextAlign.Center,
|
|
||||||
font,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
canvas.DrawText(emp.InternalSafeGet("Data1"), 100, 200, SKTextAlign.Left, font, paint);
|
|
||||||
canvas.DrawText(emp.InternalSafeGet("Data10"), 100, 240, SKTextAlign.Left, font, paint);
|
|
||||||
|
|
||||||
canvas.DrawText(comp.InternalSafeGet("Address"), 500, 200, SKTextAlign.Left, font, paint);
|
|
||||||
canvas.DrawText(
|
|
||||||
$"{comp.InternalSafeGet("City")}, {comp.InternalSafeGet("State")} {comp.InternalSafeGet("Zip")}",
|
|
||||||
500,
|
|
||||||
240,
|
|
||||||
SKTextAlign.Left,
|
|
||||||
font,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
font.Size = 80;
|
|
||||||
canvas.DrawText(
|
|
||||||
$"*{emp.InternalSafeGet("Data9")}*",
|
|
||||||
508,
|
|
||||||
500,
|
|
||||||
SKTextAlign.Center,
|
|
||||||
font,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ConvertBitmapToBase64(SKBitmap bitmap)
|
|
||||||
{
|
|
||||||
using var image = SKImage.FromBitmap(bitmap);
|
|
||||||
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
|
|
||||||
return Convert.ToBase64String(data.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<string>> DiscoverPrinters()
|
|
||||||
{
|
|
||||||
return await Task.Run(() =>
|
|
||||||
{
|
|
||||||
List<string> list = new();
|
|
||||||
var handler = new InternalStatusDiscoveryHandler();
|
|
||||||
NetworkDiscoverer.FindPrinters(handler);
|
|
||||||
int wait = 0;
|
|
||||||
while (!handler.IsDiscoveryComplete && wait < 5000)
|
|
||||||
{
|
|
||||||
Thread.Sleep(200);
|
|
||||||
wait += 200;
|
|
||||||
}
|
|
||||||
foreach (var p in handler.DiscoveredPrinters)
|
|
||||||
list.Add(p.Address);
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool ok, string error)> PrintBadgeImages(
|
|
||||||
string front,
|
|
||||||
string back,
|
|
||||||
string ip
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(ip))
|
|
||||||
return (false, "No IP");
|
|
||||||
Connection connection = new TcpConnection(ip, TcpConnection.DEFAULT_ZPL_TCP_PORT);
|
|
||||||
Zebra.Sdk.Card.Printer.ZebraCardPrinter? cardPrinter = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
connection.Open();
|
AppLogger.Info($"PrinterService: Attempting to print to {printerName}");
|
||||||
cardPrinter = ZebraCardPrinterFactory.GetInstance(connection);
|
|
||||||
using ZebraCardGraphics g = new ZebraCardGraphics(cardPrinter);
|
using (PrintDocument pd = new PrintDocument())
|
||||||
List<Zebra.Sdk.Card.Containers.GraphicsInfo> info = new();
|
|
||||||
if (!string.IsNullOrEmpty(front))
|
|
||||||
{
|
{
|
||||||
g.DrawImage(
|
pd.PrinterSettings.PrinterName = printerName;
|
||||||
Convert.FromBase64String(front),
|
pd.DefaultPageSettings.Landscape = true;
|
||||||
0,
|
pd.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0);
|
||||||
0,
|
|
||||||
0,
|
bool frontPrinted = false;
|
||||||
0,
|
|
||||||
(Zebra.Sdk.Card.Graphics.Enumerations.RotationType)0
|
pd.PrintPage += (sender, e) =>
|
||||||
);
|
|
||||||
info.Add(
|
|
||||||
new Zebra.Sdk.Card.Containers.GraphicsInfo
|
|
||||||
{
|
{
|
||||||
Side = Zebra.Sdk.Card.Enumerations.CardSide.Front,
|
try
|
||||||
PrintType = Zebra.Sdk.Card.Enumerations.PrintType.Color,
|
{
|
||||||
GraphicData = g.CreateImage(),
|
string currentB64 = !frontPrinted ? frontBase64 : backBase64;
|
||||||
}
|
|
||||||
|
using (var ms = new MemoryStream(Convert.FromBase64String(currentB64)))
|
||||||
|
using (var img = System.Drawing.Image.FromStream(ms))
|
||||||
|
{
|
||||||
|
if (img.Height > img.Width)
|
||||||
|
{
|
||||||
|
e.Graphics.TranslateTransform(e.PageBounds.Width, 0);
|
||||||
|
e.Graphics.RotateTransform(90);
|
||||||
|
e.Graphics.DrawImage(
|
||||||
|
img,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
e.PageBounds.Height,
|
||||||
|
e.PageBounds.Width
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
cardPrinter.Print(1, info);
|
else
|
||||||
return (true, "");
|
{
|
||||||
|
e.Graphics.DrawImage(
|
||||||
|
img,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
e.PageBounds.Width,
|
||||||
|
e.PageBounds.Height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frontPrinted && !string.IsNullOrEmpty(backBase64))
|
||||||
|
{
|
||||||
|
frontPrinted = true;
|
||||||
|
e.HasMorePages = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.HasMorePages = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return (false, ex.Message);
|
AppLogger.Error("PrinterService: Error during PrintPage event", ex);
|
||||||
|
e.HasMorePages = false;
|
||||||
}
|
}
|
||||||
finally
|
};
|
||||||
|
|
||||||
|
pd.Print();
|
||||||
|
AppLogger.Info("PrinterService: Print job sent to spooler.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
cardPrinter?.Destroy();
|
AppLogger.Error("PrinterService: Critical failure in PrintBase64Badge", ex);
|
||||||
if (connection.Connected)
|
|
||||||
connection.Close();
|
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
AppLogger.Warn("PrinterService: PrintBase64Badge called on non-Windows platform.");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed missing classes and helpers
|
|
||||||
public class InternalStatusDiscoveryHandler : DiscoveryHandler
|
|
||||||
{
|
|
||||||
public List<DiscoveredPrinter> DiscoveredPrinters { get; } = new();
|
|
||||||
public bool IsDiscoveryComplete { get; private set; } = false;
|
|
||||||
|
|
||||||
public void FoundPrinter(DiscoveredPrinter printer) => DiscoveredPrinters.Add(printer);
|
|
||||||
|
|
||||||
public void DiscoveryFinished() => IsDiscoveryComplete = true;
|
|
||||||
|
|
||||||
public void DiscoveryError(string message) => IsDiscoveryComplete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class PrinterExtensions
|
|
||||||
{
|
|
||||||
public static string InternalSafeGet(this Dictionary<string, object> dict, string key) =>
|
|
||||||
dict.TryGetValue(key, out var val) && val != null ? val.ToString()! : "";
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,40 @@ public class SqlService
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
|
|
||||||
public SqlService(IConfiguration configuration)
|
// Change constructor to accept string instead of IConfiguration
|
||||||
|
public SqlService(string connectionString)
|
||||||
{
|
{
|
||||||
// Pulls the "Default" string from your JSON structure
|
_connectionString = connectionString;
|
||||||
_connectionString =
|
|
||||||
configuration.GetConnectionString("Default")
|
if (string.IsNullOrEmpty(_connectionString))
|
||||||
?? throw new Exception("Connection string 'Default' not found in appsettings.json");
|
{
|
||||||
|
AppLogger.Warn("SqlService: Initialized with an EMPTY connection string.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Dictionary<string, object>> Query(string sql, SqlParameter[]? parameters = null)
|
public List<Dictionary<string, object>> Query(string sql, SqlParameter[]? parameters = null)
|
||||||
{
|
{
|
||||||
var rows = new List<Dictionary<string, object>>();
|
var rows = new List<Dictionary<string, object>>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_connectionString))
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
$"SqlService: Skipping Query because connection string is missing. SQL: {sql}"
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
using var conn = new SqlConnection(_connectionString);
|
using var conn = new SqlConnection(_connectionString);
|
||||||
using var cmd = new SqlCommand(sql, conn);
|
using var cmd = new SqlCommand(sql, conn);
|
||||||
|
|
||||||
if (parameters != null)
|
if (parameters != null)
|
||||||
cmd.Parameters.AddRange(parameters);
|
cmd.Parameters.AddRange(parameters);
|
||||||
|
|
||||||
|
AppLogger.Debug($"SqlService: Executing Query: {sql}");
|
||||||
conn.Open();
|
conn.Open();
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|
@ -35,16 +52,50 @@ public class SqlService
|
||||||
}
|
}
|
||||||
rows.Add(row);
|
rows.Add(row);
|
||||||
}
|
}
|
||||||
|
AppLogger.Info($"SqlService: Query returned {rows.Count} rows.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error($"SqlService: Query execution failed for: {sql}", ex);
|
||||||
|
// We re-throw here because this happens after the app has already started
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(string sql, SqlParameter[]? parameters = null)
|
public void Execute(string sql, SqlParameter[]? parameters = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_connectionString))
|
||||||
|
{
|
||||||
|
AppLogger.Warn($"SqlService: Skipping Execute because connection string is missing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
using var conn = new SqlConnection(_connectionString);
|
using var conn = new SqlConnection(_connectionString);
|
||||||
using var cmd = new SqlCommand(sql, conn);
|
using var cmd = new SqlCommand(sql, conn);
|
||||||
|
|
||||||
if (parameters != null)
|
if (parameters != null)
|
||||||
|
{
|
||||||
|
// Clear any previous ownership just in case
|
||||||
|
cmd.Parameters.Clear();
|
||||||
cmd.Parameters.AddRange(parameters);
|
cmd.Parameters.AddRange(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.Debug($"SqlService: Executing Command: {sql}");
|
||||||
conn.Open();
|
conn.Open();
|
||||||
cmd.ExecuteNonQuery();
|
int affected = cmd.ExecuteNonQuery();
|
||||||
|
AppLogger.Info($"SqlService: Execute finished. Rows affected: {affected}");
|
||||||
|
|
||||||
|
// IMPORTANT: Clear parameters after execution so they can be reused if needed
|
||||||
|
cmd.Parameters.Clear();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error($"SqlService: Execute failed for: {sql}", ex);
|
||||||
|
throw; // This will trigger the DisplayAlert in your Page
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentPage
|
||||||
|
x:Class="FrymasterBadgeApp.SettingsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
Title="Settings">
|
||||||
|
<ScrollView>
|
||||||
|
<VerticalStackLayout Padding="30" Spacing="20">
|
||||||
|
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="24"
|
||||||
|
Text="Configuration"
|
||||||
|
TextColor="{StaticResource Primary}" />
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="8">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="16"
|
||||||
|
Text="Theme" />
|
||||||
|
|
||||||
|
<Picker
|
||||||
|
x:Name="ThemePicker"
|
||||||
|
Title="Select theme"
|
||||||
|
TextColor="{AppThemeBinding Light={StaticResource TextLight}, Dark={StaticResource TextDark}}">
|
||||||
|
<Picker.Items>
|
||||||
|
<x:String>System</x:String>
|
||||||
|
<x:String>Light</x:String>
|
||||||
|
<x:String>Dark</x:String>
|
||||||
|
</Picker.Items>
|
||||||
|
</Picker>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<BoxView HeightRequest="1"
|
||||||
|
Color="{AppThemeBinding Light={StaticResource BorderLight}, Dark={StaticResource BorderDark}}" />
|
||||||
|
|
||||||
|
<VerticalStackLayout Spacing="10">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="16"
|
||||||
|
Text="Badge Photo Folder" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
FontSize="12"
|
||||||
|
Text="Where employee photos are stored"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
|
|
||||||
|
<HorizontalStackLayout Spacing="10">
|
||||||
|
<Entry
|
||||||
|
x:Name="PathEntry"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Placeholder="Select folder..."
|
||||||
|
FontSize="15"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Clicked="OnPickPhotoFolderClicked"
|
||||||
|
Text="Browse..."
|
||||||
|
WidthRequest="100" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<VerticalStackLayout Margin="0,10,0,0" Spacing="10">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="16"
|
||||||
|
Text="Logo Folder" />
|
||||||
|
|
||||||
|
<HorizontalStackLayout Spacing="10">
|
||||||
|
<Entry
|
||||||
|
x:Name="LogoPathEntry"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Placeholder="Select folder..."
|
||||||
|
FontSize="15"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Clicked="OnPickLogoFolderClicked"
|
||||||
|
Text="Browse..."
|
||||||
|
WidthRequest="100" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<VerticalStackLayout Margin="0,10,0,0" Spacing="10">
|
||||||
|
<Label
|
||||||
|
FontAttributes="Bold"
|
||||||
|
FontSize="16"
|
||||||
|
Text="Additional Images Folder" />
|
||||||
|
|
||||||
|
<HorizontalStackLayout Spacing="10">
|
||||||
|
<Entry
|
||||||
|
x:Name="ImagesPathEntry"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Placeholder="Select folder..."
|
||||||
|
FontSize="15"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Clicked="OnPickImagesFolderClicked"
|
||||||
|
Text="Browse..."
|
||||||
|
WidthRequest="100" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
x:Name="SaveButton"
|
||||||
|
Margin="0,30,0,0"
|
||||||
|
BackgroundColor="{StaticResource Primary}"
|
||||||
|
Clicked="OnSaveClicked"
|
||||||
|
CornerRadius="8"
|
||||||
|
HeightRequest="50"
|
||||||
|
Text="Save Settings"
|
||||||
|
TextColor="White" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
x:Name="StatusLabel"
|
||||||
|
Margin="5,8,0,0"
|
||||||
|
FontSize="13"
|
||||||
|
TextColor="{StaticResource Success}" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
Margin="0,20,0,0"
|
||||||
|
FontSize="11"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
Text="Tip: Use the Browse buttons to select folders easily"
|
||||||
|
TextColor="{StaticResource Gray500}" />
|
||||||
|
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</ContentPage>
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
using CommunityToolkit.Maui.Alerts;
|
||||||
|
using CommunityToolkit.Maui.Storage;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Storage;
|
||||||
|
|
||||||
|
namespace FrymasterBadgeApp;
|
||||||
|
|
||||||
|
public partial class SettingsPage : ContentPage
|
||||||
|
{
|
||||||
|
private const string PhotoPathKey = "PhotoBasePath";
|
||||||
|
private const string LogoPathKey = "LogoBasePath";
|
||||||
|
private const string ImagesPathKey = "ImagesBasePath";
|
||||||
|
private const string ThemePrefKey = "AppTheme";
|
||||||
|
|
||||||
|
public SettingsPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
LoadCurrentSettings();
|
||||||
|
LoadCurrentThemePreference();
|
||||||
|
|
||||||
|
ThemePicker.SelectedIndexChanged += (s, e) => ApplyThemeSelection();
|
||||||
|
|
||||||
|
Shell.SetBackButtonBehavior(
|
||||||
|
this,
|
||||||
|
new BackButtonBehavior
|
||||||
|
{
|
||||||
|
TextOverride = "Back",
|
||||||
|
Command = new Command(async () => await Shell.Current.GoToAsync("..")),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCurrentSettings()
|
||||||
|
{
|
||||||
|
PathEntry.Text = Preferences.Default.Get(PhotoPathKey, @"C:\FrymasterData\photos");
|
||||||
|
LogoPathEntry.Text = Preferences.Default.Get(LogoPathKey, @"C:\FrymasterData\logos");
|
||||||
|
ImagesPathEntry.Text = Preferences.Default.Get(ImagesPathKey, @"C:\FrymasterData\images");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCurrentThemePreference()
|
||||||
|
{
|
||||||
|
var pref = Preferences.Default.Get(ThemePrefKey, "System");
|
||||||
|
switch (pref)
|
||||||
|
{
|
||||||
|
case "Light":
|
||||||
|
ThemePicker.SelectedIndex = 1;
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Light;
|
||||||
|
break;
|
||||||
|
case "Dark":
|
||||||
|
ThemePicker.SelectedIndex = 2;
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Dark;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ThemePicker.SelectedIndex = 0;
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Unspecified;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyThemeSelection()
|
||||||
|
{
|
||||||
|
if (ThemePicker == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sel = ThemePicker.SelectedIndex;
|
||||||
|
switch (sel)
|
||||||
|
{
|
||||||
|
case 1: // Light
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Light;
|
||||||
|
Preferences.Default.Set(ThemePrefKey, "Light");
|
||||||
|
break;
|
||||||
|
case 2: // Dark
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Dark;
|
||||||
|
Preferences.Default.Set(ThemePrefKey, "Dark");
|
||||||
|
break;
|
||||||
|
default: // System
|
||||||
|
Application.Current.UserAppTheme = AppTheme.Unspecified;
|
||||||
|
Preferences.Default.Set(ThemePrefKey, "System");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnPickPhotoFolderClicked(object sender, EventArgs e) =>
|
||||||
|
await PickFolderAsync(r => PathEntry.Text = r.Folder.Path, "Select Photos Folder");
|
||||||
|
|
||||||
|
private async void OnPickLogoFolderClicked(object sender, EventArgs e) =>
|
||||||
|
await PickFolderAsync(r => LogoPathEntry.Text = r.Folder.Path, "Select Logos Folder");
|
||||||
|
|
||||||
|
private async void OnPickImagesFolderClicked(object sender, EventArgs e) =>
|
||||||
|
await PickFolderAsync(r => ImagesPathEntry.Text = r.Folder.Path, "Select Images Folder");
|
||||||
|
|
||||||
|
private async Task PickFolderAsync(Action<FolderPickerResult> onSuccess, string title)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await FolderPicker.Default.PickAsync(title, CancellationToken.None);
|
||||||
|
if (result.IsSuccessful)
|
||||||
|
{
|
||||||
|
onSuccess(result);
|
||||||
|
VerifyDirectory(result.Folder.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await DisplayAlert("Error", ex.Message, "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSaveClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
AppLogger.Info("Save settings clicked - start");
|
||||||
|
|
||||||
|
var photoPath = PathEntry?.Text?.Trim();
|
||||||
|
var logoPath = LogoPathEntry?.Text?.Trim();
|
||||||
|
var imagesPath = ImagesPathEntry?.Text?.Trim();
|
||||||
|
|
||||||
|
AppLogger.Info(
|
||||||
|
$"Paths collected → Photos: '{photoPath ?? "null"}', Logos: '{logoPath ?? "null"}', Images: '{imagesPath ?? "null"}'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
string.IsNullOrWhiteSpace(photoPath)
|
||||||
|
|| string.IsNullOrWhiteSpace(logoPath)
|
||||||
|
|| string.IsNullOrWhiteSpace(imagesPath)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
AppLogger.Warning("Validation failed - one or more paths empty");
|
||||||
|
await DisplayAlert("Error", "All folders must be selected.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppLogger.Info("Saving preferences...");
|
||||||
|
|
||||||
|
Preferences.Default.Set(PhotoPathKey, photoPath);
|
||||||
|
Preferences.Default.Set(LogoPathKey, logoPath);
|
||||||
|
Preferences.Default.Set(ImagesPathKey, imagesPath);
|
||||||
|
|
||||||
|
AppLogger.Info("Preferences saved OK");
|
||||||
|
|
||||||
|
StatusLabel.Text = "Settings saved successfully!";
|
||||||
|
StatusLabel.TextColor = Colors.Green;
|
||||||
|
|
||||||
|
// Toast is optional & wrapped — comment out if still crashes
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
await Toast
|
||||||
|
.Make("Settings saved ✓", CommunityToolkit.Maui.Core.ToastDuration.Short)
|
||||||
|
.Show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception toastEx)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("Toast failed (non-critical)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO auto-navigation — let user press back button manually
|
||||||
|
// This avoids 90% of known Shell navigation crashes
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error("Critical save failure", ex);
|
||||||
|
|
||||||
|
StatusLabel.Text = $"Save failed: {ex.Message}";
|
||||||
|
StatusLabel.TextColor = Colors.Red;
|
||||||
|
|
||||||
|
await DisplayAlert(
|
||||||
|
"Save Error",
|
||||||
|
$"{ex.GetType().Name}: {ex.Message}\n\nCheck logs for details.",
|
||||||
|
"OK"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
AppLogger.Info("Save settings handler finished");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private static void SafeCreateDirectory(string? path)
|
||||||
|
// {
|
||||||
|
// if (string.IsNullOrWhiteSpace(path)) return;
|
||||||
|
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// if (!Directory.Exists(path))
|
||||||
|
// {
|
||||||
|
// AppLogger.Info($"Creating directory: {path}");
|
||||||
|
// Directory.CreateDirectory(path);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// AppLogger.Info($"Directory already exists: {path}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// AppLogger.Error($"Failed to create/verify directory '{path}'", ex);
|
||||||
|
// throw; // rethrow so main catch can handle it
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Helper - prevents crash on invalid paths
|
||||||
|
private static void SafeCreateDirectory(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log but don't crash the app
|
||||||
|
System.Diagnostics.Debug.WriteLine(
|
||||||
|
$"Directory creation failed for '{path}': {ex.Message}"
|
||||||
|
);
|
||||||
|
// You could also show a warning here if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyDirectory(string path)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
/**
|
||||||
|
* DeComPress CMS - Core Admin Logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
currentView: 'Pages',
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Minimalist',
|
||||||
|
version: '1.2.0',
|
||||||
|
active: true,
|
||||||
|
color: 'bg-slate-300'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Dark Mode Pro',
|
||||||
|
version: '2.0.1',
|
||||||
|
active: false,
|
||||||
|
color: 'bg-gray-800'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Ocean Breeze',
|
||||||
|
version: '1.0.5',
|
||||||
|
active: false,
|
||||||
|
color: 'bg-blue-400'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
customFields: [], // Dynamic state for the field builder
|
||||||
|
navItems: [
|
||||||
|
{ name: 'Pages', icon: '📄' },
|
||||||
|
{ name: 'Themes', icon: '🎨' },
|
||||||
|
{ name: 'Post Types', icon: '📌' },
|
||||||
|
{ name: 'Custom Fields', icon: '🔧' },
|
||||||
|
{ name: 'Users', icon: '👥' },
|
||||||
|
{ name: 'Roles', icon: '🛡️' },
|
||||||
|
{ name: 'Permissions', icon: '🔑' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initialization ---
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
function initApp () {
|
||||||
|
renderNav()
|
||||||
|
navigateTo(state.currentView)
|
||||||
|
|
||||||
|
// Mobile Toggle Logic
|
||||||
|
document.getElementById('mobile-toggle').addEventListener('click', () => {
|
||||||
|
const sidebar = document.getElementById('sidebar')
|
||||||
|
sidebar.classList.toggle('hidden')
|
||||||
|
sidebar.classList.toggle('absolute')
|
||||||
|
sidebar.classList.toggle('z-50')
|
||||||
|
sidebar.classList.toggle('h-full')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Router ---
|
||||||
|
function navigateTo (viewName) {
|
||||||
|
state.currentView = viewName
|
||||||
|
document.getElementById('view-title').innerText = viewName
|
||||||
|
renderNav() // Refresh active state
|
||||||
|
|
||||||
|
const container = document.getElementById('app-content')
|
||||||
|
container.innerHTML = '' // Clear current
|
||||||
|
|
||||||
|
switch (viewName) {
|
||||||
|
case 'Pages':
|
||||||
|
container.innerHTML = viewPages()
|
||||||
|
break
|
||||||
|
case 'Themes':
|
||||||
|
container.innerHTML = viewThemes()
|
||||||
|
break
|
||||||
|
case 'Permissions':
|
||||||
|
container.innerHTML = viewPermissions()
|
||||||
|
break
|
||||||
|
case 'Custom Fields':
|
||||||
|
container.innerHTML = viewCustomFields()
|
||||||
|
break
|
||||||
|
case 'Users':
|
||||||
|
container.innerHTML = viewUsers()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
container.innerHTML = `<div class="p-10 text-center border-2 border-dashed rounded-lg text-gray-400">
|
||||||
|
${viewName} module is currently under construction.
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Views ---
|
||||||
|
|
||||||
|
function viewPages () {
|
||||||
|
return `
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<thead class="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th class="p-4 font-semibold text-gray-600">Page Title</th>
|
||||||
|
<th class="p-4 font-semibold text-gray-600">Status</th>
|
||||||
|
<th class="p-4 font-semibold text-gray-600 text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
<tr><td class="p-4">Homepage</td><td class="p-4"><span class="bg-green-100 text-green-700 px-2 py-1 rounded text-xs">Published</span></td><td class="p-4 text-right text-blue-600 cursor-pointer">Edit</td></tr>
|
||||||
|
<tr><td class="p-4">Contact</td><td class="p-4"><span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded text-xs">Draft</span></td><td class="p-4 text-right text-blue-600 cursor-pointer">Edit</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewThemes () {
|
||||||
|
return `
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
${state.themes
|
||||||
|
.map(
|
||||||
|
t => `
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border-2 ${
|
||||||
|
t.active ? 'border-blue-500' : 'border-transparent'
|
||||||
|
} overflow-hidden">
|
||||||
|
<div class="h-32 ${t.color}"></div>
|
||||||
|
<div class="p-4">
|
||||||
|
<h4 class="font-bold">${t.name}</h4>
|
||||||
|
<p class="text-xs text-gray-500 mb-4">Version ${
|
||||||
|
t.version
|
||||||
|
}</p>
|
||||||
|
<button onclick="handleAction('Theme ${
|
||||||
|
t.name
|
||||||
|
} activated')" class="w-full py-2 rounded text-sm font-medium ${
|
||||||
|
t.active
|
||||||
|
? 'bg-blue-50 text-blue-600'
|
||||||
|
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
|
||||||
|
}">
|
||||||
|
${t.active ? 'Active' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewPermissions () {
|
||||||
|
const roles = ['Admin', 'Editor', 'Subscriber']
|
||||||
|
const caps = ['Update Core', 'Manage Themes', 'Delete Posts', 'Write Posts']
|
||||||
|
return `
|
||||||
|
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-200">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<thead class="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th class="p-4 text-gray-600">Capability</th>
|
||||||
|
${roles
|
||||||
|
.map(
|
||||||
|
r =>
|
||||||
|
`<th class="p-4 text-center text-gray-600">${r}</th>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
${caps
|
||||||
|
.map(
|
||||||
|
c => `
|
||||||
|
<tr>
|
||||||
|
<td class="p-4 font-medium">${c}</td>
|
||||||
|
${roles
|
||||||
|
.map(
|
||||||
|
r =>
|
||||||
|
`<td class="p-4 text-center"><input type="checkbox" ${
|
||||||
|
r === 'Admin' ? 'checked' : ''
|
||||||
|
} class="w-4 h-4 rounded border-gray-300"></td>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewCustomFields () {
|
||||||
|
return `
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-lg font-bold">Field Group: Blog Metadata</h3>
|
||||||
|
<button onclick="addField()" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm hover:bg-blue-700 transition">+ Add Field</button>
|
||||||
|
</div>
|
||||||
|
<div id="fields-container" class="space-y-4">
|
||||||
|
${
|
||||||
|
state.customFields.length === 0
|
||||||
|
? '<p class="text-gray-400 italic text-center py-10">No fields added yet. Click "+ Add Field" to begin.</p>'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewUsers () {
|
||||||
|
return `
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-sm max-w-lg border border-gray-200">
|
||||||
|
<h3 class="font-bold mb-4">Invite New User</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Email Address</label>
|
||||||
|
<input type="email" id="u-email" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<button onclick="handleUserInvite()" class="bg-slate-900 text-white w-full py-2 rounded-md font-medium hover:bg-black transition">Send Invitation</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Component Logic ---
|
||||||
|
|
||||||
|
function renderNav () {
|
||||||
|
const nav = document.getElementById('main-nav')
|
||||||
|
nav.innerHTML = state.navItems
|
||||||
|
.map(
|
||||||
|
item => `
|
||||||
|
<button onclick="navigateTo('${item.name}')"
|
||||||
|
class="w-full flex items-center px-4 py-3 rounded-lg text-sm transition-colors ${
|
||||||
|
state.currentView === item.name
|
||||||
|
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
|
||||||
|
: 'text-gray-400 hover:bg-slate-800 hover:text-white'
|
||||||
|
}">
|
||||||
|
<span class="mr-3 text-lg">${item.icon}</span>
|
||||||
|
<span class="font-medium">${item.name}</span>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function addField () {
|
||||||
|
const id = Date.now()
|
||||||
|
state.customFields.push({ id, label: '', type: 'text' })
|
||||||
|
refreshFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeField (id) {
|
||||||
|
state.customFields = state.customFields.filter(f => f.id !== id)
|
||||||
|
refreshFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFields () {
|
||||||
|
const container = document.getElementById('fields-container')
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
container.innerHTML = state.customFields
|
||||||
|
.map(
|
||||||
|
(field, index) => `
|
||||||
|
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm flex items-center space-x-4 animate-in fade-in duration-300">
|
||||||
|
<div class="text-gray-300 cursor-move">☰</div>
|
||||||
|
<div class="flex-1 grid grid-cols-2 gap-4">
|
||||||
|
<input type="text" placeholder="Field Label" class="border rounded p-2 text-sm" value="${field.label}">
|
||||||
|
<select class="border rounded p-2 text-sm">
|
||||||
|
<option>Text</option>
|
||||||
|
<option>Textarea</option>
|
||||||
|
<option>Image</option>
|
||||||
|
<option>Boolean (Toggle)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button onclick="removeField(${field.id})" class="text-red-400 hover:text-red-600 p-2">✕</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- General Handlers ---
|
||||||
|
|
||||||
|
function handleUserInvite () {
|
||||||
|
const email = document.getElementById('u-email').value
|
||||||
|
if (!email.includes('@')) {
|
||||||
|
alert('Please enter a valid email address.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alert(`Invitation sent to ${email}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAction (msg) {
|
||||||
|
console.log('CMS Action:', msg)
|
||||||
|
alert(msg)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue