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? _selectedEmployee; private readonly Dictionary _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> _allEmployees = new(); private ObservableCollection> _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 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("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); } } /// /// Resolves full logo path: filename from Companies table + folder from Settings /// 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(); } }); } /// /// Loads the list of employees from the database. /// The results are ordered by employee name (ascending). /// 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(); }); } /// /// Positions the search results dropdown below the search bar. /// private void PositionSearchResultsDropdown() { if (SearchResultsList != null) { SearchResultsList.TranslationY = 60; SearchResultsList.ZIndex = 100; } } /// /// 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. /// /// The object that invoked this method. /// The event data for this method. private void OnEmployeeSelected(object sender, SelectedItemChangedEventArgs e) { if (e.SelectedItem is not Dictionary 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; } /// /// 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. /// /// The object that invoked this method. /// The event data for this method. 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"); } } /// /// Helper to find the employee in the list, highlight it, and scroll to it. /// private void SelectAndShowEmployee(Dictionary 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); } } }); } /// /// Opens the EmployeeFormPage for editing the selected employee. /// If the form is saved, it refreshes the list and shows the updated employee. /// /// The object that invoked this method. /// The event data for this method. 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"); } } /// /// Called when the "Badge Type" dropdown changes. /// Saves the new badge type to the database and updates the badge preview. /// /// The object that invoked this method. /// The event data for this method. 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); } /// /// Called when the "Guest Image" dropdown changes. /// Updates the preview of the badge if the badge type is GUEST. /// /// The object that invoked this method. /// The event data for this method. private void OnGuestImageChanged(object sender, EventArgs e) { _currentGuestImage = GuestImagePicker?.SelectedItem?.ToString() ?? "Guest1.png"; if (_selectedEmployee != null && _currentBadgeType == "GUEST") RenderBadgePreview(_selectedEmployee); } /// /// Deploys the default images (Guest.png and WelBilt.png) if they are not already present in the images directory. /// 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); } } } } /// /// Renders the preview for a badge given the employee's data. /// /// The employee's data. private void RenderBadgePreview(Dictionary 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); } /// /// Adds a standard front to the badge layout, including the logo (if supplied), employee photo, and name. /// /// The layout to add the front to. /// The path to the logo image. /// The employee data. /// The record number to display on the front. /// The special footer text to display on the front. /// Whether to display a blue medical symbol on the front. /// Whether to display a red medical symbol on the front. private void AddStandardFront( VerticalStackLayout layout, string logoPath, Dictionary 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); } } /// /// Adds a standard set of details to the back of the badge preview, /// including company name, address, ID, date, and medical indicators. /// /// The layout to add the details to. /// The name of the company. /// The address of the company. /// The ID of the badge. /// The date the badge was issued. /// Whether to show the company logo. /// The path to the company logo. /// Whether to show the medical blue indicator. /// Whether to show the medical red indicator. 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); } /// /// 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. /// /// The object that invoked this method. /// The event data for this method. /// Thrown if there is an error taking the photo. 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"); } } /// /// Called when the user is panning the photo. /// /// The object that invoked this method. /// The event data for this method. /// /// 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. /// 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; } } } /// /// Called when the user wants to apply the edited photo to the record. /// /// The object that invoked this method. /// The event data for this method. /// /// Copies the edited photo to the final location, updates the database, and renders the badge preview. /// 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"); } } /// /// Hides the photo editor overlay when the user clicks the cancel button. /// /// The object that invoked this method. /// The event data for this method. private void OnCancelPhoto(object sender, EventArgs e) { if (PhotoEditorOverlay != null) PhotoEditorOverlay.IsVisible = false; } /// /// Safely converts an object to a double value. If the object is null or DBNull.Value, returns a default value. /// /// The object to convert. /// The value to return if the object is null or DBNull.Value. /// The double value of the object, or the default value if the object is null or DBNull.Value. 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; } /// /// Prints the selected employee's badge using the selected printer. /// /// The object that invoked this method. /// The event data for this method. 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"); } } /// /// Converts a screenshot result stream to a base64 string. /// /// The screenshot result stream to convert. /// The base64 string representation of the stream. private async Task StreamToBase64(IScreenshotResult shot) { using var stream = await shot.OpenReadAsync(); using var ms = new MemoryStream(); await stream.CopyToAsync(ms); return Convert.ToBase64String(ms.ToArray()); } }