1279 lines
44 KiB
C#
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());
|
|
}
|
|
}
|