FrymasterBadgeApp/EmployeePage.xaml.cs

1279 lines
44 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing.Printing;
using FrymasterBadgeApp.Services;
using Microsoft.Data.SqlClient;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Storage;
namespace FrymasterBadgeApp;
public partial class EmployeePage : ContentPage
{
private readonly SqlService _db;
private readonly PrinterService _printerService;
private Dictionary<string, object>? _selectedEmployee;
private readonly Dictionary<string, object> _currentCompany;
// UI controls (explicitly declared and resolved via FindByName to avoid missing generated fields)
// Use distinct field names to avoid ambiguity with generated XAML fields
// UI controls are defined in XAML (x:Name) and provided by the generated partial class.
// Avoid manual field declarations to prevent ambiguity with generated fields.
private ObservableCollection<Dictionary<string, object>> _allEmployees = new();
private ObservableCollection<Dictionary<string, object>> _filteredEmployees = new();
private string _tempCapturePath = "";
private double _editX = 0;
private double _editY = 0;
private string _currentBadgeType = "OFFICE";
private string _currentGuestImage = "Guest1.png";
// Paths from Settings
private readonly string _photosBasePath;
private readonly string _logosBasePath;
private readonly string _imagesBasePath;
private bool _showActiveOnly = true;
public EmployeePage(
SqlService db,
PrinterService printerService,
Dictionary<string, object> company
)
{
// Use generated InitializeComponent (supports tooling & hot-reload) instead of loading XAML at runtime
InitializeComponent();
// Named XAML controls (x:Name) are available via generated partial class fields.
// No manual FindByName assignments are necessary here.
_db = db;
_printerService = printerService;
_currentCompany = company;
_selectedEmployee = null;
_photosBasePath = Preferences.Default.Get("PhotoBasePath", @"C:\FrymasterData\photos");
_logosBasePath = Preferences.Default.Get("LogoBasePath", @"C:\FrymasterData\logos");
_imagesBasePath = Preferences.Default.Get("ImagesBasePath", @"C:\FrymasterData\images");
EnsureDirectoryExists(_photosBasePath);
EnsureDirectoryExists(_logosBasePath);
EnsureDirectoryExists(_imagesBasePath);
// Use FindByName to locate the checkbox from XAML in case generated field isn't available
var activeCheck = this.FindByName<CheckBox>("ActiveFilterCheckBox");
if (activeCheck != null)
{
activeCheck.IsChecked = _showActiveOnly;
activeCheck.CheckedChanged += OnActiveFilterChanged;
}
else
{
AppLogger.Warning("ActiveFilterCheckBox not found in XAML.");
}
if (EditorPhotoPreview != null && ZoomSlider != null)
{
EditorPhotoPreview.SetBinding(
Image.ScaleProperty,
new Binding("Value", source: ZoomSlider)
);
}
_editX = _editY = 0;
if (EditorPhotoPreview != null)
{
EditorPhotoPreview.TranslationX = 0;
EditorPhotoPreview.TranslationY = 0;
}
if (ZoomSlider != null)
ZoomSlider.Value = 1;
if (GuestImagePicker != null)
{
GuestImagePicker.Items.Add("Guest1.png");
GuestImagePicker.SelectedIndex = 0;
GuestImagePicker.SelectedIndexChanged += OnGuestImageChanged;
}
if (BadgeTypePicker != null)
BadgeTypePicker.SelectedIndexChanged += OnBadgeTypeChanged;
if (SearchResultsList != null)
SearchResultsList.ItemsSource = _filteredEmployees;
LoadCompanyLogo();
LoadEmployees();
ShowNoSelectionMessage();
}
private void ShowNoSelectionMessage()
{
PreviewFrame.Content = new VerticalStackLayout
{
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.Center,
Spacing = 10,
Children =
{
new Label
{
Text = "Select an employee",
FontSize = 24,
TextColor = Colors.Gray,
FontAttributes = FontAttributes.Italic,
},
new Label
{
Text = "to view badge preview",
FontSize = 16,
TextColor = Colors.LightGray,
},
},
};
}
private static void EnsureDirectoryExists(string path)
{
if (string.IsNullOrWhiteSpace(path) || Directory.Exists(path))
return;
try
{
Directory.CreateDirectory(path);
}
catch (Exception ex)
{
AppLogger.Error($"Failed to create directory: {path}", ex);
}
}
/// <summary>
/// Resolves full logo path: filename from Companies table + folder from Settings
/// </summary>
private string GetCompanyLogoPath()
{
string fileName = _currentCompany.GetValueOrDefault("Logo")?.ToString()?.Trim();
if (string.IsNullOrWhiteSpace(fileName))
{
AppLogger.Info("No logo filename in company data");
return string.Empty;
}
string fullPath = Path.Combine(_logosBasePath, fileName);
if (File.Exists(fullPath))
{
AppLogger.Info($"Company logo found: {fullPath}");
return fullPath;
}
AppLogger.Warning($"Company logo missing at: {fullPath}");
return string.Empty;
}
private void LoadCompanyLogo()
{
string logoPath = GetCompanyLogoPath();
}
private string GetPhotoPath(string picCode)
{
return Path.Combine(_photosBasePath, $"{picCode}.jpg");
}
private string GetGuestImagePath(string guestImageName)
{
return Path.Combine(_imagesBasePath, guestImageName);
}
protected override async void OnAppearing()
{
base.OnAppearing();
// This effectively "closes" the previous state
ResetPageToDefault();
LoadAvailablePrinters();
// Then re-loads the data fresh from the DB
await LoadEmployeesAsync();
}
private void ResetPageToDefault()
{
// 1. Wipe the selection reference
_selectedEmployee = null;
// 2. Clear the UI Search Bar text
if (EmployeeSearchBar != null)
{
// We unsubscribe to prevent 'ApplyFilters' from running
// while we are manually clearing the text.
EmployeeSearchBar.TextChanged -= OnSearchTextChanged;
EmployeeSearchBar.Text = string.Empty;
EmployeeSearchBar.TextChanged += OnSearchTextChanged;
}
// 3. Reset the visual results list
if (SearchResultsList != null)
{
SearchResultsList.SelectedItem = null;
// SearchResultsList.IsVisible = false;
_filteredEmployees.Clear();
}
// 4. Reset the Preview to the placeholder message
ShowNoSelectionMessage();
AppLogger.Info("EmployeePage: UI Reset completed.");
}
private async Task LoadEmployeesAsync()
{
try
{
// We do not filter by company here — load all employees
var results = await Task.Run(() =>
_db.Query(
"SELECT *, ISNULL(BadgeType, 'OFFICE') AS BadgeType FROM dbo.tblData ORDER BY Data2",
null
)
);
MainThread.BeginInvokeOnMainThread(() =>
{
_allEmployees.Clear();
foreach (var emp in results)
{
_allEmployees.Add(emp);
}
// Re-run ApplyFilters if there is already text in the search bar
ApplyFilters();
AppLogger.Info(
$"EmployeePage: Loaded {_allEmployees.Count} records for {_currentCompany.GetValueOrDefault("Name")}"
);
});
}
catch (Exception ex)
{
AppLogger.Error("EmployeePage: Load failed", ex);
}
}
private void LoadAvailablePrinters()
{
if (PrinterPicker != null)
{
PrinterPicker.Items.Clear();
foreach (string printer in PrinterSettings.InstalledPrinters)
PrinterPicker.Items.Add(printer);
if (PrinterPicker.Items.Count > 0)
PrinterPicker.SelectedIndex = 0;
}
}
private void OnRefreshPrintersClicked(object sender, EventArgs e) => LoadAvailablePrinters();
private void OnActiveFilterChanged(object sender, CheckedChangedEventArgs e)
{
_showActiveOnly = e.Value;
ApplyFilters();
}
private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
{
PositionSearchResultsDropdown();
SearchResultsList.IsVisible = true;
ApplyFilters();
}
private void ApplyFilters()
{
string filter = EmployeeSearchBar?.Text?.Trim()?.ToLower() ?? "";
var filtered = _allEmployees.AsEnumerable();
if (_showActiveOnly)
filtered = filtered.Where(emp =>
string.Equals(
emp.GetValueOrDefault("Active")?.ToString(),
"YES",
StringComparison.OrdinalIgnoreCase
)
);
if (!string.IsNullOrEmpty(filter))
filtered = filtered.Where(emp =>
emp["Data2"]?.ToString()?.ToLower().Contains(filter) == true
|| emp["Data1"]?.ToString()?.Contains(filter) == true
);
var finalList = filtered.ToList();
MainThread.BeginInvokeOnMainThread(() =>
{
_filteredEmployees.Clear();
foreach (var emp in finalList)
_filteredEmployees.Add(emp);
if (SearchResultsList != null)
{
SearchResultsList.ItemsSource = _filteredEmployees;
/// SearchResultsList.IsVisible = finalList.Any();
}
});
}
/// <summary>
/// Loads the list of employees from the database.
/// The results are ordered by employee name (ascending).
/// </summary>
private void LoadEmployees()
{
string sql =
"SELECT *, ISNULL(BadgeType, 'OFFICE') AS BadgeType FROM dbo.tblData ORDER BY Data2";
var results = _db.Query(sql, null);
MainThread.BeginInvokeOnMainThread(() =>
{
_allEmployees.Clear();
foreach (var emp in results)
_allEmployees.Add(emp);
// ApplyFilters();
});
}
/// <summary>
/// Positions the search results dropdown below the search bar.
/// </summary>
private void PositionSearchResultsDropdown()
{
if (SearchResultsList != null)
{
SearchResultsList.TranslationY = 60;
SearchResultsList.ZIndex = 100;
}
}
/// <summary>
/// Called when the selection in the employees list changes.
/// Sets the selected employee, updates the search bar text, resolves the badge type with case sensitivity handling, and updates the UI accordingly.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private void OnEmployeeSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem is not Dictionary<string, object> selectedEmp)
return;
_selectedEmployee = selectedEmp;
// 1. Update Search Bar text
if (EmployeeSearchBar != null)
EmployeeSearchBar.Text = selectedEmp.GetValueOrDefault("Data2")?.ToString();
// 2. Resolve Badge Type with Case Sensitivity handling
string dbValue = selectedEmp.GetValueOrDefault("BadgeType")?.ToString()?.Trim() ?? "OFFICE";
var matchingItem = BadgeTypePicker?.Items.FirstOrDefault(i =>
string.Equals(i, dbValue, StringComparison.OrdinalIgnoreCase)
);
if (matchingItem != null && BadgeTypePicker != null)
{
BadgeTypePicker.SelectedItem = matchingItem;
_currentBadgeType = matchingItem; // Sync our internal variable
}
else if (BadgeTypePicker != null)
{
BadgeTypePicker.SelectedItem = "OFFICE";
_currentBadgeType = "OFFICE";
}
if (GuestImageSelector != null)
GuestImageSelector.IsVisible = _currentBadgeType.Equals(
"GUEST11",
StringComparison.OrdinalIgnoreCase
);
RenderBadgePreview(selectedEmp);
if (SearchResultsList != null)
SearchResultsList.IsVisible = false;
if (sender is ListView lv)
lv.SelectedItem = null;
}
/// <summary>
/// Called when the "Add Employee" button is clicked.
/// Shows a modal form for adding a new employee.
/// The form is given a callback to refresh and select the new employee when it is saved.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private async void OnAddEmployeeClicked(object sender, EventArgs e)
{
try
{
var form = new EmployeeFormPage(_db);
// Set the callback to refresh and select the new employee
form.OnSavedCallback = async (savedEmployee) =>
{
await LoadEmployeesAsync();
SelectAndShowEmployee(savedEmployee);
};
await Navigation.PushModalAsync(form);
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.Message, "OK");
}
}
/// <summary>
/// Helper to find the employee in the list, highlight it, and scroll to it.
/// </summary>
private void SelectAndShowEmployee(Dictionary<string, object> target)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var recordId = target.GetValueOrDefault("Data1")?.ToString();
// Find the item in our collection
var itemToSelect = _allEmployees.FirstOrDefault(x =>
x.GetValueOrDefault("Data1")?.ToString() == recordId
);
if (itemToSelect != null)
{
_selectedEmployee = itemToSelect;
// Clear search bar so the item is visible in the list
if (EmployeeSearchBar != null)
EmployeeSearchBar.Text = itemToSelect.GetValueOrDefault("Data2")?.ToString();
// Update the Preview
RenderBadgePreview(itemToSelect);
// Note: Since you are using a custom dropdown SearchResultsList,
// we ensure it updates its selection visually
if (SearchResultsList != null)
{
SearchResultsList.SelectedItem = itemToSelect;
// Note: ListView.ScrollTo requires the item, Group (null), and Position
SearchResultsList.ScrollTo(itemToSelect, ScrollToPosition.Center, true);
}
}
});
}
/// <summary>
/// Opens the EmployeeFormPage for editing the selected employee.
/// If the form is saved, it refreshes the list and shows the updated employee.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private async void OnEditEmployeeClicked(object sender, EventArgs e)
{
if (_selectedEmployee == null)
{
await DisplayAlert("No Selection", "Please select an employee first.", "OK");
return;
}
try
{
var formPage = new EmployeeFormPage(_db, _selectedEmployee);
await Navigation.PushModalAsync(formPage);
if (formPage.IsSaved)
{
LoadEmployees();
RenderBadgePreview(_selectedEmployee);
}
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.Message, "OK");
}
}
/// <summary>
/// Called when the "Badge Type" dropdown changes.
/// Saves the new badge type to the database and updates the badge preview.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private void OnBadgeTypeChanged(object sender, EventArgs e)
{
if (_selectedEmployee == null)
return;
_currentBadgeType = BadgeTypePicker?.SelectedItem?.ToString() ?? "OFFICE";
if (GuestImageSelector != null)
GuestImageSelector.IsVisible = _currentBadgeType == "GUEST";
string sql = "UPDATE dbo.tblData SET BadgeType = @type WHERE Data1 = @id";
_db.Execute(
sql,
new[]
{
new SqlParameter("@type", _currentBadgeType),
new SqlParameter("@id", _selectedEmployee["Data1"]),
}
);
_selectedEmployee["BadgeType"] = _currentBadgeType;
RenderBadgePreview(_selectedEmployee);
}
/// <summary>
/// Called when the "Guest Image" dropdown changes.
/// Updates the preview of the badge if the badge type is GUEST.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private void OnGuestImageChanged(object sender, EventArgs e)
{
_currentGuestImage = GuestImagePicker?.SelectedItem?.ToString() ?? "Guest1.png";
if (_selectedEmployee != null && _currentBadgeType == "GUEST")
RenderBadgePreview(_selectedEmployee);
}
/// <summary>
/// Deploys the default images (Guest.png and WelBilt.png) if they are not already present in the images directory.
/// </summary>
private async Task InitialiseDefaultImages()
{
string[] defaultFiles = { "Guest.png", "WelBilt.png" };
foreach (var fileName in defaultFiles)
{
string targetPath = Path.Combine(_imagesBasePath, fileName);
if (!File.Exists(targetPath))
{
try
{
// This looks into your Resources\Raw folder
using var stream = await FileSystem.OpenAppPackageFileAsync(fileName);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
File.WriteAllBytes(targetPath, memoryStream.ToArray());
AppLogger.Info($"Deployed default image: {fileName} to {targetPath}");
}
catch (Exception ex)
{
AppLogger.Error($"Could not deploy default image {fileName}", ex);
}
}
}
}
/// <summary>
/// Renders the preview for a badge given the employee's data.
/// </summary>
/// <param name="emp">The employee's data.</param>
private void RenderBadgePreview(Dictionary<string, object> emp)
{
string companyName = _currentCompany.GetValueOrDefault("Name")?.ToString() ?? "Frymaster";
string logoPath = GetCompanyLogoPath(); // ← correct company logo resolution
string formattedAddress =
$"{_currentCompany.GetValueOrDefault("Address")}\n"
+ $"{_currentCompany.GetValueOrDefault("City")}, "
+ $"{_currentCompany.GetValueOrDefault("State")} {_currentCompany.GetValueOrDefault("Zip")}";
string recordNum = (emp.GetValueOrDefault("Data1")?.ToString() ?? "").Trim();
string barcode = (emp.GetValueOrDefault("Data9")?.ToString() ?? "").Trim();
string date = (emp.GetValueOrDefault("Data5")?.ToString() ?? "").Trim();
var mainLayout = new HorizontalStackLayout
{
Spacing = 40,
HorizontalOptions = LayoutOptions.Center,
};
var frontLayout = new VerticalStackLayout { Spacing = 5 };
var backLayout = new VerticalStackLayout
{
Spacing = 0,
VerticalOptions = LayoutOptions.Fill,
};
var frontFrame = new Frame
{
Padding = 15,
WidthRequest = 300,
HeightRequest = 480,
BackgroundColor = Colors.White,
BorderColor = Colors.White,
CornerRadius = 15,
HasShadow = true,
Content = frontLayout,
};
var backFrame = new Frame
{
WidthRequest = 480,
HeightRequest = 300,
BackgroundColor = Colors.White,
BorderColor = Colors.White,
CornerRadius = 15,
Padding = 0,
HasShadow = true,
Content = backLayout,
};
bool medicalBlue = string.Equals(
emp.GetValueOrDefault("Data7")?.ToString(),
"True",
StringComparison.OrdinalIgnoreCase
);
bool medicalRed = string.Equals(
emp.GetValueOrDefault("Data8")?.ToString(),
"True",
StringComparison.OrdinalIgnoreCase
);
switch (_currentBadgeType)
{
case "GUEST":
frontFrame.BorderColor = Colors.Purple;
// 1. Company Logo
if (!string.IsNullOrEmpty(logoPath))
{
frontLayout.Children.Add(new Image
{
Source = ImageSource.FromFile(logoPath),
HeightRequest = 55,
Margin = new Thickness(0, 0, 0, 10),
});
}
// 2. Resolve Path
string guestImg = _currentGuestImage ?? "Guest.jpg";
var guestImageControl = new Image
{
HeightRequest = 220,
WidthRequest = 220,
Aspect = Aspect.AspectFit,
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20),
BackgroundColor = Colors.White // Keep this until it works!
};
string guestPath = GetGuestImagePath(_currentGuestImage);
// Add this line temporarily:
//DisplayAlert("Path Check", $"Path: {guestPath}\nExists: {File.Exists(guestPath)}", "OK");
// 3. Robust Stream Loading
if (File.Exists(guestPath))
{
// Loading via stream bypasses many "FromFile" permission issues
guestImageControl.Source = ImageSource.FromStream(() => File.OpenRead(guestPath));
}
else
{
// Fallback so you know the file is physically missing from the disk
guestImageControl.Source = "dotnet_bot.png";
}
frontLayout.Children.Add(guestImageControl);
AddStandardBackDetails(backLayout, companyName, formattedAddress, recordNum, date, false, logoPath, medicalBlue, medicalRed);
break;
case "PLANT":
case "MAINTENANCE": // "Maintenance":
Color stripeColor = (_currentBadgeType == "PLANT") ? Colors.Red : Colors.Yellow;
frontFrame.BorderColor =
(_currentBadgeType == "PLANT") ? Colors.Green : Colors.Orange;
AddStandardFront(
frontLayout,
logoPath,
emp,
recordNum,
"",
medicalBlue,
medicalRed
);
var stripeGrid = new Grid
{
BackgroundColor = stripeColor,
HorizontalOptions = LayoutOptions.Fill,
HeightRequest = 45,
Margin = new Thickness(0, 35, 0, 0),
};
stripeGrid.Children.Add(
new Label
{
Text = barcode,
FontFamily = "BarcodeFont",
FontSize = 75,
Padding = 0,
TextColor = Colors.Black,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
LineHeight = 0.8,
Margin = new Thickness(0, 0, 0, -25),
}
);
backLayout.Children.Add(stripeGrid);
AddStandardBackDetails(
backLayout,
companyName,
formattedAddress,
recordNum,
date,
false,
logoPath,
medicalBlue,
medicalRed
);
break;
case "VENDOR":
frontFrame.BorderColor = Colors.Red;
AddStandardFront(frontLayout, logoPath, emp, "", "VENDOR", medicalBlue, medicalRed);
AddStandardBackDetails(
backLayout,
companyName,
formattedAddress,
recordNum,
date,
false,
logoPath,
medicalBlue,
medicalRed
);
break;
case "OFFICE":
frontFrame.BorderColor = Colors.Blue;
AddStandardFront(
frontLayout,
logoPath,
emp,
recordNum,
"",
medicalBlue,
medicalRed
);
AddStandardBackDetails(
backLayout,
companyName,
formattedAddress,
recordNum,
date,
true,
logoPath,
medicalBlue,
medicalRed
);
break;
}
mainLayout.Children.Add(frontFrame);
mainLayout.Children.Add(backFrame);
MainThread.BeginInvokeOnMainThread(() => PreviewFrame.Content = mainLayout);
}
/// <summary>
/// Adds a standard front to the badge layout, including the logo (if supplied), employee photo, and name.
/// </summary>
/// <param name="layout">The layout to add the front to.</param>
/// <param name="logoPath">The path to the logo image.</param>
/// <param name="emp">The employee data.</param>
/// <param name="recordNum">The record number to display on the front.</param>
/// <param name="specialFooter">The special footer text to display on the front.</param>
/// <param name="medicalBlue">Whether to display a blue medical symbol on the front.</param>
/// <param name="medicalRed">Whether to display a red medical symbol on the front.</param>
private void AddStandardFront(
VerticalStackLayout layout,
string logoPath,
Dictionary<string, object> emp,
string recordNum,
string specialFooter,
bool medicalBlue = false,
bool medicalRed = false
)
{
if (!string.IsNullOrEmpty(logoPath))
layout.Children.Add(
new Image
{
Source = ImageSource.FromFile(logoPath),
HeightRequest = 55,
Margin = new Thickness(0, 0, 0, 10),
}
);
string picCode = emp.GetValueOrDefault("picCode")?.ToString() ?? "";
string photoPath = GetPhotoPath(picCode);
var photoContainer = new Grid
{
HeightRequest = 180,
WidthRequest = 180,
IsClippedToBounds = true,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
};
var photoImg = new Image
{
Aspect = Aspect.AspectFill,
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Fill,
};
if (File.Exists(photoPath))
{
photoImg.Source = ImageSource.FromFile(photoPath);
double scale = SafeToDouble(emp.GetValueOrDefault("cropScale"), 1.0);
double cropX = SafeToDouble(emp.GetValueOrDefault("cropX"), 0.0);
double cropY = SafeToDouble(emp.GetValueOrDefault("cropY"), 0.0);
photoImg.Scale = scale;
photoImg.TranslationX = cropX;
photoImg.TranslationY = cropY;
}
photoContainer.Children.Add(photoImg);
layout.Children.Add(photoContainer);
layout.Children.Add(
new Label
{
Text = (emp.GetValueOrDefault("Data3")?.ToString() ?? "").ToUpper(),
FontSize = 36,
FontAttributes = FontAttributes.Bold,
TextColor = Colors.Black,
HorizontalTextAlignment = TextAlignment.Center,
}
);
layout.Children.Add(
new Label
{
Text = (emp.GetValueOrDefault("Data2")?.ToString() ?? ""),
FontSize = 18,
TextColor = Colors.Black,
HorizontalTextAlignment = TextAlignment.Center,
}
);
layout.Children.Add(
new Label
{
Text = "*" + (recordNum ?? "") + "*",
FontFamily = "BarcodeFont",
FontSize = 45,
TextColor = Colors.Black,
HorizontalTextAlignment = TextAlignment.Center,
}
);
if (medicalBlue || medicalRed)
{
var medStack = new HorizontalStackLayout
{
HorizontalOptions = LayoutOptions.Center,
Spacing = 20,
};
if (medicalBlue)
medStack.Children.Add(
new Label
{
Text = "*",
FontSize = 48,
FontAttributes = FontAttributes.Bold,
TextColor = Colors.Blue,
}
);
if (medicalRed)
medStack.Children.Add(
new Label
{
Text = "*",
FontSize = 48,
FontAttributes = FontAttributes.Bold,
TextColor = Colors.Red,
}
);
layout.Children.Add(medStack);
}
}
/// <summary>
/// Adds a standard set of details to the back of the badge preview,
/// including company name, address, ID, date, and medical indicators.
/// </summary>
/// <param name="layout">The layout to add the details to.</param>
/// <param name="company">The name of the company.</param>
/// <param name="address">The address of the company.</param>
/// <param name="id">The ID of the badge.</param>
/// <param name="date">The date the badge was issued.</param>
/// <param name="showLogo">Whether to show the company logo.</param>
/// <param name="logoPath">The path to the company logo.</param>
/// <param name="medicalBlue">Whether to show the medical blue indicator.</param>
/// <param name="medicalRed">Whether to show the medical red indicator.</param>
private void AddStandardBackDetails(
VerticalStackLayout layout,
string company,
string address,
string id,
string date,
bool showLogo,
string logoPath,
bool medicalBlue,
bool medicalRed
)
{
var textContainer = new VerticalStackLayout { Padding = 20, Spacing = 5 };
if (showLogo && !string.IsNullOrEmpty(logoPath))
textContainer.Children.Add(
new Image
{
Source = ImageSource.FromFile(logoPath),
HeightRequest = 40,
Margin = new Thickness(0, 10, 0, 5),
}
);
textContainer.Children.Add(
new Label
{
Text = $"This badge is property of {company}",
FontAttributes = FontAttributes.Bold,
FontSize = 16,
HorizontalTextAlignment = TextAlignment.Center,
TextColor = Colors.Blue,
}
);
textContainer.Children.Add(
new Label
{
Text = "If found, please return to Human Resources.",
FontSize = 16,
HorizontalTextAlignment = TextAlignment.Center,
TextColor = Colors.Black,
}
);
textContainer.Children.Add(
new Label
{
Text = address,
FontSize = 16,
TextColor = Colors.DarkSlateGray,
HorizontalTextAlignment = TextAlignment.Center,
}
);
textContainer.Children.Add(
new BoxView
{
Color = Colors.LightGray,
HeightRequest = 1,
Margin = new Thickness(20, 10),
}
);
var grid = new Grid
{
ColumnDefinitions =
{
new ColumnDefinition { Width = GridLength.Auto }, // Left side fits the ID
new ColumnDefinition { Width = GridLength.Star }, // Right side takes remaining space
},
WidthRequest = 440, // Reduced slightly to ensure it fits inside the 480 frame padding
Margin = new Thickness(20, 0),
};
// First Label (Column 0)
var idLabel = new Label
{
Text = id,
FontAttributes = FontAttributes.Bold,
FontSize = 16,
TextColor = Colors.Black,
VerticalTextAlignment = TextAlignment.Center,
};
Grid.SetColumn(idLabel, 0); // <--- Explicitly set column 0
// Second Label (Column 1)
var issuedLabel = new Label
{
Text = $"Issued: {date}",
FontSize = 16,
TextColor = Colors.Black,
HorizontalOptions = LayoutOptions.End,
HorizontalTextAlignment = TextAlignment.End,
VerticalTextAlignment = TextAlignment.Center,
};
Grid.SetColumn(issuedLabel, 1); // <--- Explicitly set column 1
// Add them to the grid
grid.Children.Add(idLabel);
grid.Children.Add(issuedLabel);
textContainer.Children.Add(grid);
if (medicalBlue || medicalRed)
{
var medStack = new HorizontalStackLayout
{
HorizontalOptions = LayoutOptions.Center,
Spacing = 30,
};
if (medicalBlue)
medStack.Children.Add(
new Label
{
Text = "*",
FontSize = 40,
FontAttributes = FontAttributes.Bold,
TextColor = Colors.Blue,
}
);
if (medicalRed)
medStack.Children.Add(
new Label
{
Text = "*",
FontSize = 40,
FontAttributes = FontAttributes.Bold,
TextColor = Colors.Red,
}
);
textContainer.Children.Add(medStack);
}
layout.Children.Add(textContainer);
}
/// <summary>
/// Called when the "Take Photo" button is clicked.
/// Opens the camera and takes a photo. If successful, updates the photo preview and resets the editor state.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
/// <exception cref="Exception">Thrown if there is an error taking the photo.</exception>
private async void OnTakePhotoClicked(object sender, EventArgs e)
{
if (_selectedEmployee == null)
return;
try
{
var photo = await MediaPicker.Default.CapturePhotoAsync();
if (photo == null)
return;
_tempCapturePath = Path.Combine(FileSystem.CacheDirectory, "temp_editor.jpg");
using var source = await photo.OpenReadAsync();
using var destination = File.OpenWrite(_tempCapturePath);
await source.CopyToAsync(destination);
if (EditorPhotoPreview != null)
EditorPhotoPreview.Source = ImageSource.FromFile(_tempCapturePath);
_editX = _editY = 0;
if (EditorPhotoPreview != null)
{
EditorPhotoPreview.TranslationX = 0;
EditorPhotoPreview.TranslationY = 0;
}
if (ZoomSlider != null)
ZoomSlider.Value = 1;
if (PhotoEditorOverlay != null)
PhotoEditorOverlay.IsVisible = true;
}
catch (Exception ex)
{
await DisplayAlert("Camera Error", ex.Message, "OK");
}
}
/// <summary>
/// Called when the user is panning the photo.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
/// <remarks>
/// If the pan is running, updates the translation of the photo based on the total pan distance.
/// If the pan is completed, updates the stored edit position based on the final translation of the photo.
/// </remarks>
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (e.StatusType == GestureStatus.Running)
{
if (EditorPhotoPreview != null)
{
EditorPhotoPreview.TranslationX = _editX + e.TotalX;
EditorPhotoPreview.TranslationY = _editY + e.TotalY;
}
}
else if (e.StatusType == GestureStatus.Completed)
{
if (EditorPhotoPreview != null)
{
_editX = EditorPhotoPreview.TranslationX;
_editY = EditorPhotoPreview.TranslationY;
}
}
}
/// <summary>
/// Called when the user wants to apply the edited photo to the record.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
/// <remarks>
/// Copies the edited photo to the final location, updates the database, and renders the badge preview.
/// </remarks>
private async void OnApplyPhoto(object sender, EventArgs e)
{
if (_selectedEmployee == null)
return;
try
{
string recordNum =
_selectedEmployee.GetValueOrDefault("Data1")?.ToString()?.Trim() ?? "";
string picCode = $"{recordNum}_{DateTime.Now.Ticks}";
string finalPath = Path.Combine(_photosBasePath, $"{picCode}.jpg");
File.Copy(_tempCapturePath, finalPath, true);
string sql =
@"UPDATE dbo.tblData SET picCode = @pic, cropScale = @s, cropX = @x, cropY = @y WHERE Data1 = @id";
_db.Execute(
sql,
new[]
{
new SqlParameter("@pic", picCode),
new SqlParameter("@s", ZoomSlider?.Value ?? 1.0),
new SqlParameter("@x", EditorPhotoPreview?.TranslationX ?? 0.0),
new SqlParameter("@y", EditorPhotoPreview?.TranslationY ?? 0.0),
new SqlParameter("@id", recordNum),
}
);
_selectedEmployee["picCode"] = picCode;
_selectedEmployee["cropScale"] = ZoomSlider?.Value ?? 1.0;
_selectedEmployee["cropX"] = EditorPhotoPreview?.TranslationX ?? 0.0;
_selectedEmployee["cropY"] = EditorPhotoPreview?.TranslationY ?? 0.0;
RenderBadgePreview(_selectedEmployee);
if (PhotoEditorOverlay != null)
PhotoEditorOverlay.IsVisible = false;
}
catch (Exception ex)
{
await DisplayAlert("Save Error", ex.Message, "OK");
}
}
/// <summary>
/// Hides the photo editor overlay when the user clicks the cancel button.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private void OnCancelPhoto(object sender, EventArgs e)
{
if (PhotoEditorOverlay != null)
PhotoEditorOverlay.IsVisible = false;
}
/// <summary>
/// Safely converts an object to a double value. If the object is null or DBNull.Value, returns a default value.
/// </summary>
/// <param name="value">The object to convert.</param>
/// <param name="defaultValue">The value to return if the object is null or DBNull.Value.</param>
/// <returns>The double value of the object, or the default value if the object is null or DBNull.Value.</returns>
private double SafeToDouble(object? value, double defaultValue = 1.0)
{
if (value == null || value == DBNull.Value)
return defaultValue;
return double.TryParse(value.ToString(), out double d) ? d : defaultValue;
}
/// <summary>
/// Prints the selected employee's badge using the selected printer.
/// </summary>
/// <param name="sender">The object that invoked this method.</param>
/// <param name="e">The event data for this method.</param>
private async void OnPrintClicked(object sender, EventArgs e)
{
if (_selectedEmployee == null || PrinterPicker?.SelectedItem == null)
{
await DisplayAlert("Missing Information", "Select an employee and a printer.", "OK");
return;
}
try
{
if (
PreviewFrame.Content is HorizontalStackLayout mainLayout
&& mainLayout.Children.Count >= 2
)
{
var frontView = mainLayout.Children[0] as VisualElement;
var backView = mainLayout.Children[1] as VisualElement;
if (frontView == null || backView == null)
return;
var frontResult = await frontView.CaptureAsync();
var backResult = await backView.CaptureAsync();
string frontB64 = await StreamToBase64(frontResult);
string backB64 = await StreamToBase64(backResult);
_printerService.PrintBase64Badge(
frontB64,
backB64,
PrinterPicker.SelectedItem.ToString()!
);
await DisplayAlert("Success", "Badge sent to printer.", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Print Error", ex.Message, "OK");
}
}
/// <summary>
/// Converts a screenshot result stream to a base64 string.
/// </summary>
/// <param name="shot">The screenshot result stream to convert.</param>
/// <returns>The base64 string representation of the stream.</returns>
private async Task<string> StreamToBase64(IScreenshotResult shot)
{
using var stream = await shot.OpenReadAsync();
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
return Convert.ToBase64String(ms.ToArray());
}
}