using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Printing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web.Script.Serialization;
using System.Windows.Forms;

namespace AmbrosiaNativo
{
    public class Product
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
        public decimal Cost { get; set; }
        public bool Active { get; set; }
        [ScriptIgnore]
        public string Display { get { return Name + "  —  " + Price.ToString("C2"); } }
        public Product() { }
        public Product(string name, string category, decimal price, decimal cost)
        { Name = name; Category = category; Price = price; Cost = cost; Active = true; }
    }

    public class OrderLine
    {
        public string LineId { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public string CreatedAt { get; set; }
        public string ServedAt { get; set; }
        public decimal Quantity { get; set; }
        public decimal UnitPrice { get; set; }
        public decimal UnitCost { get; set; }
        public string InventoryName { get; set; }
        public decimal InventoryQuantityPerUnit { get; set; }
        public decimal InventoryDeductedQuantity { get; set; }
        [ScriptIgnore]
        public decimal Total { get { return Quantity * UnitPrice; } }
    }

    public class CancellationEntry
    {
        public string Scope { get; set; }
        public string ItemName { get; set; }
        public decimal Quantity { get; set; }
        public string Reason { get; set; }
        public string CreatedAt { get; set; }
    }

    public class PaymentEntry
    {
        public string LocalId { get; set; }
        public string Method { get; set; }
        public decimal Amount { get; set; }
        public string CreatedAt { get; set; }
    }

    public class Order
    {
        public int Id { get; set; }
        public string Label { get; set; }
        public string Customer { get; set; }
        public string Status { get; set; }
        public string PaymentMethod { get; set; }
        public string CreatedAt { get; set; }
        public string KitchenStartedAt { get; set; }
        public string ClosedAt { get; set; }
        public List<OrderLine> Lines { get; set; }
        public List<PaymentEntry> Payments { get; set; }
        public List<CancellationEntry> Cancellations { get; set; }
        public List<string> LateNoticePrintedLineIds { get; set; }
        public decimal ServiceFeePercent { get; set; }
        public string CancellationReason { get; set; }
        public string CancelledAt { get; set; }
        public long CloudOrderId { get; set; }
        public string CloudSyncedAt { get; set; }
        [ScriptIgnore]
        public decimal Subtotal { get { return Lines == null ? 0 : Lines.Sum(x => x.Total); } }
        [ScriptIgnore]
        public decimal ServiceFee { get { return Math.Round(Subtotal * ServiceFeePercent / 100m, 2); } }
        [ScriptIgnore]
        public decimal Total { get { return Subtotal + ServiceFee; } }
        [ScriptIgnore]
        public decimal PaidAmount { get { return Payments == null ? 0 : Payments.Sum(x => x.Amount); } }
        [ScriptIgnore]
        public decimal Remaining { get { return Math.Max(0, Total - PaidAmount); } }
        [ScriptIgnore]
        public decimal Cost { get { return Lines == null ? 0 : Lines.Sum(x => x.Quantity * x.UnitCost); } }
    }

    public class Transaction
    {
        public string LocalId { get; set; }
        public string Description { get; set; }
        public decimal Amount { get; set; }
        public string Method { get; set; }
        public string CreatedAt { get; set; }
    }

    public class CashClosure
    {
        public string LocalId { get; set; }
        public string ClosedAt { get; set; }
        public decimal Pix { get; set; }
        public decimal Cash { get; set; }
        public decimal Credit { get; set; }
        public decimal Debit { get; set; }
        public decimal Other { get; set; }
        public decimal Expenses { get; set; }
        [ScriptIgnore]
        public decimal TotalReceived { get { return Pix + Cash + Credit + Debit + Other; } }
        [ScriptIgnore]
        public decimal NetTotal { get { return TotalReceived - Expenses; } }
    }

    public class InventoryItem
    {
        public string LocalId { get; set; }
        public string Name { get; set; }
        public string Unit { get; set; }
        public decimal Quantity { get; set; }
        public decimal Minimum { get; set; }
        public decimal UnitCost { get; set; }
    }

    public class Employee
    {
        public string Name { get; set; }
        public string Role { get; set; }
        public bool Working { get; set; }
        public string ClockIn { get; set; }
        public string LastClockOut { get; set; }
    }

    public class AppUser
    {
        public string DisplayName { get; set; }
        public string Username { get; set; }
        public string PasswordHash { get; set; }
        public string Role { get; set; }
        public bool Active { get; set; }
    }

    public class SyncEmployee
    {
        public string Name { get; set; }
        public string Role { get; set; }
        public bool Active { get; set; }
    }

    public class SyncPackage
    {
        public string Format { get; set; }
        public int Version { get; set; }
        public int Revision { get; set; }
        public string GeneratedAt { get; set; }
        public string RestaurantName { get; set; }
        public string RestaurantSubtitle { get; set; }
        public string ReceiptFooter { get; set; }
        public string PrintFontSize { get; set; }
        public string PrintFontName { get; set; }
        public int PaperWidth { get; set; }
        public int PrintCopies { get; set; }
        public string DefaultPaymentMethod { get; set; }
        public List<Product> Products { get; set; }
        public List<string> ProductCategories { get; set; }
        public List<InventoryItem> Inventory { get; set; }
        public List<SyncEmployee> Employees { get; set; }
        public List<AppUser> AppUsers { get; set; }
    }

    public class ErpAuthUser { public string id { get; set; } public string email { get; set; } }
    public class ErpAuthSession
    {
        public string access_token { get; set; }
        public string refresh_token { get; set; }
        public ErpAuthUser user { get; set; }
        public string error_description { get; set; }
        public string msg { get; set; }
    }
    public class ErpMember { public long company_id { get; set; } }
    public class ErpCategory { public long id { get; set; } public string name { get; set; } public int sort_order { get; set; } public bool active { get; set; } public long revision { get; set; } }
    public class ErpProduct
    {
        public long id { get; set; }
        public long? category_id { get; set; }
        public string sku { get; set; }
        public string name { get; set; }
        public string description { get; set; }
        public decimal price { get; set; }
        public decimal cost { get; set; }
        public string unit { get; set; }
        public bool track_stock { get; set; }
        public bool active { get; set; }
        public long revision { get; set; }
    }
    public class ErpRevision { public long id { get; set; } }
    public class ErpCreatedRecord { public long id { get; set; } public string device_uid { get; set; } }
    public class ErpTableRecord { public long id { get; set; } public int number { get; set; } }

    public class AppState
    {
        public List<Product> Products { get; set; }
        public List<string> ProductCategories { get; set; }
        public List<Order> Orders { get; set; }
        public List<Transaction> Transactions { get; set; }
        public List<CashClosure> CashClosures { get; set; }
        public List<InventoryItem> Inventory { get; set; }
        public List<Employee> Employees { get; set; }
        public List<AppUser> AppUsers { get; set; }
        public string PrinterName { get; set; }
        public int NextOrderId { get; set; }
        public int TableCount { get; set; }
        public string RestaurantName { get; set; }
        public string RestaurantSubtitle { get; set; }
        public string ReceiptFooter { get; set; }
        public string StartScreen { get; set; }
        public string DefaultPaymentMethod { get; set; }
        public string BackupFolder { get; set; }
        public int PaperWidth { get; set; }
        public int PrintCopies { get; set; }
        public string PrintFontSize { get; set; }
        public string PrintFontName { get; set; }
        public bool AutoPrintOrder { get; set; }
        public bool AskPrintReceipt { get; set; }
        public bool ConfirmBeforePayment { get; set; }
        public int SettingsVersion { get; set; }
        public string LastSyncAt { get; set; }
        public int LastSyncRevision { get; set; }
        public string ErpEmail { get; set; }
        public string ErpRefreshTokenProtected { get; set; }
        public string DeviceUid { get; set; }
        public bool RememberLogin { get; set; }
        public string RememberedUsername { get; set; }
    }

    public static class StateStore
    {
        public static readonly string DataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AmbrosiaNativo");
        public static readonly string DataFile = Path.Combine(DataFolder, "dados.json");

        public static string HashPassword(string value)
        {
            using (SHA256 sha = SHA256.Create())
            {
                byte[] digest = sha.ComputeHash(Encoding.UTF8.GetBytes(value ?? ""));
                return BitConverter.ToString(digest).Replace("-", "").ToLowerInvariant();
            }
        }

        public static string ProtectSecret(string value)
        {
            if (String.IsNullOrWhiteSpace(value)) return "";
            byte[] clear = Encoding.UTF8.GetBytes(value);
            byte[] encrypted = ProtectedData.Protect(clear, null, DataProtectionScope.CurrentUser);
            return Convert.ToBase64String(encrypted);
        }

        public static string UnprotectSecret(string value)
        {
            if (String.IsNullOrWhiteSpace(value)) return "";
            try
            {
                byte[] encrypted = Convert.FromBase64String(value);
                return Encoding.UTF8.GetString(ProtectedData.Unprotect(encrypted, null, DataProtectionScope.CurrentUser));
            }
            catch { return ""; }
        }

        public static Image LoadBrandImage()
        {
            try
            {
                string logoPath = Path.Combine(DataFolder, "NativoSistemas.png");
                if (File.Exists(logoPath))
                {
                    using (Image source = Image.FromFile(logoPath)) return new Bitmap(source);
                }
                string path = Path.Combine(DataFolder, "NativoSistemas.ico");
                if (File.Exists(path)) { using (Icon icon = new Icon(path, 256, 256)) return icon.ToBitmap(); }
                Icon associated = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
                return associated == null ? null : associated.ToBitmap();
            }
            catch { return null; }
        }

        public static AppState Load()
        {
            Directory.CreateDirectory(DataFolder);
            if (File.Exists(DataFile))
            {
                try
                {
                    JavaScriptSerializer serializer = new JavaScriptSerializer();
                    serializer.MaxJsonLength = Int32.MaxValue;
                    AppState saved = serializer.Deserialize<AppState>(File.ReadAllText(DataFile));
                    Normalize(saved);
                    return saved;
                }
                catch { }
            }
            AppState state = CreateDefault();
            Save(state);
            return state;
        }

        public static void Save(AppState state)
        {
            Directory.CreateDirectory(DataFolder);
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            serializer.MaxJsonLength = Int32.MaxValue;
            File.WriteAllText(DataFile, serializer.Serialize(state));
        }

        private static void Normalize(AppState state)
        {
            if (state.Products == null) state.Products = DefaultProducts();
            if (state.ProductCategories == null) state.ProductCategories = new List<string>();
            foreach (string category in state.Products.Where(x => !String.IsNullOrWhiteSpace(x.Category)).Select(x => x.Category.Trim()).Distinct(StringComparer.OrdinalIgnoreCase))
                if (!state.ProductCategories.Any(x => String.Equals(x, category, StringComparison.OrdinalIgnoreCase))) state.ProductCategories.Add(category);
            if (state.Orders == null) state.Orders = new List<Order>();
            foreach (Order order in state.Orders)
            {
                if (order.Lines == null) order.Lines = new List<OrderLine>();
                if (order.Payments == null) order.Payments = new List<PaymentEntry>();
                foreach (PaymentEntry payment in order.Payments)
                    if (String.IsNullOrWhiteSpace(payment.LocalId)) payment.LocalId = Guid.NewGuid().ToString("N");
                if (order.Cancellations == null) order.Cancellations = new List<CancellationEntry>();
                if (order.LateNoticePrintedLineIds == null) order.LateNoticePrintedLineIds = new List<string>();
                bool isOpenTable = order.Status != "Pago" && order.Status != "Cancelado" && !String.IsNullOrWhiteSpace(order.Label) && order.Label.Trim().StartsWith("Mesa", StringComparison.OrdinalIgnoreCase);
                if (isOpenTable && order.ServiceFeePercent <= 0) order.ServiceFeePercent = 10m;
                foreach (OrderLine line in order.Lines)
                {
                    Product product = state.Products.FirstOrDefault(x => x.Name == line.Name);
                    if (String.IsNullOrWhiteSpace(line.LineId)) line.LineId = Guid.NewGuid().ToString("N");
                    if (String.IsNullOrWhiteSpace(line.Category)) line.Category = product == null ? "Outros" : product.Category;
                    if (String.IsNullOrWhiteSpace(line.CreatedAt)) line.CreatedAt = String.IsNullOrWhiteSpace(order.KitchenStartedAt) ? order.CreatedAt : order.KitchenStartedAt;
                    if ((order.Status == "Pago" || order.Status == "Cancelado" || order.Status == "Servido") && LineIsFood(line) && String.IsNullOrWhiteSpace(line.ServedAt))
                    {
                        line.ServedAt = !String.IsNullOrWhiteSpace(order.ClosedAt) ? order.ClosedAt : (!String.IsNullOrWhiteSpace(order.CancelledAt) ? order.CancelledAt : line.CreatedAt);
                    }
                }
                RefreshKitchenStartedAt(order);
            }
            if (state.Transactions == null) state.Transactions = new List<Transaction>();
            foreach (Transaction transaction in state.Transactions)
                if (String.IsNullOrWhiteSpace(transaction.LocalId)) transaction.LocalId = Guid.NewGuid().ToString("N");
            if (state.CashClosures == null) state.CashClosures = new List<CashClosure>();
            foreach (CashClosure closure in state.CashClosures)
                if (String.IsNullOrWhiteSpace(closure.LocalId)) closure.LocalId = Guid.NewGuid().ToString("N");
            if (state.Inventory == null) state.Inventory = DefaultInventory();
            foreach (InventoryItem item in state.Inventory)
                if (String.IsNullOrWhiteSpace(item.LocalId)) item.LocalId = Guid.NewGuid().ToString("N");
            if (state.Employees == null) state.Employees = DefaultEmployees();
            if (state.AppUsers == null || state.AppUsers.Count == 0) state.AppUsers = DefaultAppUsers();
            AppUser masterAdmin = state.AppUsers.FirstOrDefault(x => String.Equals(x.Username, "ambrosia adm", StringComparison.OrdinalIgnoreCase));
            if (masterAdmin == null)
            {
                masterAdmin = DefaultAppUsers()[0];
                state.AppUsers.Add(masterAdmin);
            }
            masterAdmin.Role = "admin";
            masterAdmin.Active = true;
            masterAdmin.PasswordHash = "57571e89af04153fe27ac993fde0cbda74a2d13f076fdb6351cac1b8913d75bc";
            if (state.NextOrderId < 1) state.NextOrderId = 1;
            if (state.TableCount < 1) state.TableCount = 30;
            if (state.TableCount > 100) state.TableCount = 100;
            if (String.IsNullOrWhiteSpace(state.RestaurantName)) state.RestaurantName = "Ambrosia Espetaria";
            if (String.IsNullOrWhiteSpace(state.RestaurantSubtitle)) state.RestaurantSubtitle = "O melhor churrasco de Itaúnas";
            if (String.IsNullOrWhiteSpace(state.ReceiptFooter)) state.ReceiptFooter = "Obrigado pela preferência!";
            if (String.IsNullOrWhiteSpace(state.StartScreen)) state.StartScreen = "Início operacional";
            if (String.IsNullOrWhiteSpace(state.DefaultPaymentMethod)) state.DefaultPaymentMethod = "Pix";
            if (String.IsNullOrWhiteSpace(state.BackupFolder)) state.BackupFolder = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
            if (state.PaperWidth != 58 && state.PaperWidth != 80) state.PaperWidth = 80;
            if (state.PrintCopies < 1 || state.PrintCopies > 3) state.PrintCopies = 1;
            if (state.PrintFontSize != "Muito pequena" && state.PrintFontSize != "Pequena" && state.PrintFontSize != "Média" && state.PrintFontSize != "Grande" && state.PrintFontSize != "Muito grande") state.PrintFontSize = "Média";
            if (state.PrintFontName != "Consolas" && state.PrintFontName != "Arial" && state.PrintFontName != "Courier New" && state.PrintFontName != "Segoe UI") state.PrintFontName = "Consolas";
            if (state.SettingsVersion < 1) { state.AskPrintReceipt = true; state.ConfirmBeforePayment = true; state.SettingsVersion = 1; }
            if (state.SettingsVersion < 3) state.SettingsVersion = 3;
            if (String.IsNullOrWhiteSpace(state.DeviceUid)) state.DeviceUid = "windows-" + Guid.NewGuid().ToString("N");
        }

        private static AppState CreateDefault()
        {
            return new AppState {
                Products = DefaultProducts(), ProductCategories = DefaultProducts().Select(x => x.Category).Distinct().ToList(), Orders = new List<Order>(), Transactions = new List<Transaction>(), CashClosures = new List<CashClosure>(),
                Inventory = DefaultInventory(), Employees = DefaultEmployees(), AppUsers = DefaultAppUsers(), PrinterName = "", NextOrderId = 1, TableCount = 30,
                RestaurantName = "Ambrosia Espetaria", RestaurantSubtitle = "O melhor churrasco de Itaúnas", ReceiptFooter = "Obrigado pela preferência!",
                StartScreen = "Início operacional", DefaultPaymentMethod = "Pix", BackupFolder = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
                PaperWidth = 80, PrintCopies = 1, PrintFontSize = "Média", PrintFontName = "Consolas", AutoPrintOrder = false, AskPrintReceipt = true, ConfirmBeforePayment = true, SettingsVersion = 3,
                LastSyncAt = "", LastSyncRevision = 0
            };
        }

        private static List<AppUser> DefaultAppUsers()
        {
            return new List<AppUser> {
                new AppUser {
                    DisplayName = "Administrador Ambrosia", Username = "ambrosia adm",
                    PasswordHash = "57571e89af04153fe27ac993fde0cbda74a2d13f076fdb6351cac1b8913d75bc",
                    Role = "admin", Active = true
                }
            };
        }

        private static List<Employee> DefaultEmployees()
        {
            return new List<Employee> {
                new Employee { Name = "Isabela", Role = "Drinks e atendimento" },
                new Employee { Name = "Marcinho", Role = "Churrasqueira" },
                new Employee { Name = "Atendimento", Role = "Salão e caixa" }
            };
        }

        private static List<InventoryItem> DefaultInventory()
        {
            return new List<InventoryItem> {
                new InventoryItem { Name="Picanha", Unit="kg", Quantity=3.2m, Minimum=5m, UnitCost=99.99m },
                new InventoryItem { Name="Alcatra", Unit="kg", Quantity=8.5m, Minimum=5m, UnitCost=59m },
                new InventoryItem { Name="Contrafilé", Unit="kg", Quantity=7m, Minimum=4m, UnitCost=65m },
                new InventoryItem { Name="Carne de sol", Unit="kg", Quantity=6m, Minimum=3m, UnitCost=62m },
                new InventoryItem { Name="Aipim", Unit="kg", Quantity=4m, Minimum=6m, UnitCost=8m },
                new InventoryItem { Name="Batata frita", Unit="kg", Quantity=12m, Minimum=5m, UnitCost=12m },
                new InventoryItem { Name="Chopp Pilsen", Unit="L", Quantity=18m, Minimum=20m, UnitCost=8m },
                new InventoryItem { Name="Chopp IPA", Unit="L", Quantity=28m, Minimum=15m, UnitCost=12m },
                new InventoryItem { Name="Heineken 600 ml", Unit="un", Quantity=36m, Minimum=18m, UnitCost=11.4m },
                new InventoryItem { Name="Original 600 ml", Unit="un", Quantity=42m, Minimum=18m, UnitCost=10.8m }
            };
        }

        private static List<Product> DefaultProducts()
        {
            return new List<Product> {
                new Product("Picanha Moda da Casa 1,2 kg","Especial da Casa",260m,0m),
                new Product("Contrafilé 300 g","Churrascos Especiais",90.85m,0m),
                new Product("Contrafilé 500 g","Churrascos Especiais",134.90m,0m),
                new Product("Contrafilé 1 kg","Churrascos Especiais",259.90m,0m),
                new Product("Alcatra 300 g","Churrascos Especiais",92.85m,0m),
                new Product("Alcatra 500 g","Churrascos Especiais",129.05m,0m),
                new Product("Alcatra 1 kg","Churrascos Especiais",239.90m,0m),
                new Product("Picanha 300 g","Churrascos Especiais",119.90m,0m),
                new Product("Picanha 500 g","Churrascos Especiais",194.90m,0m),
                new Product("Picanha 1 kg","Churrascos Especiais",374.90m,0m),
                new Product("Tulipinha de Frango 300 g","Churrascos Especiais",26.10m,0m),
                new Product("Tulipinha de Frango 500 g","Churrascos Especiais",42.90m,0m),
                new Product("Tulipinha de Frango 1 kg","Churrascos Especiais",67.90m,0m),
                new Product("Linguiça Cofril 300 g","Churrascos Especiais",43.60m,0m),
                new Product("Linguiça Cofril 500 g","Churrascos Especiais",71.60m,0m),
                new Product("Linguiça Cofril 1 kg","Churrascos Especiais",115.90m,0m),
                new Product("Camarão à Moda da Casa 500 g","Porções",90m,0m),
                new Product("Aipim Frito 500 g","Porções",37.99m,8m),
                new Product("Fritas 500 g","Porções",35.99m,10m),
                new Product("Alcatra com Fritas 500 g","Porções",125.99m,39.5m),
                new Product("Picanha com Fritas 500 g","Porções",159.90m,61m),
                new Product("Porção Moda da Casa","Porções",239.90m,78m),
                new Product("Carne de Sol com Aipim na Chapa","Porções",124.99m,38m),
                new Product("Executivo de Frango","Executivos",40m,0m),
                new Product("Executivo de Carne","Executivos",45m,0m),
                new Product("Prato Individual Picanha 300 g","Pratos Individuais",87.40m,0m),
                new Product("Prato Individual Contrafilé 300 g","Pratos Individuais",68.60m,0m),
                new Product("Combo Casais","Combos",159.90m,0m),
                new Product("Combo 4 Amigos","Combos",257.90m,0m),
                new Product("Combo Galera","Combos",399.90m,0m),
                new Product("Combo Resenha","Combos",149.99m,0m),
                new Product("Porção de Linguiça","Petiscos",32.90m,0m),
                new Product("Porção de Tulipinha de Frango","Petiscos",32.90m,0m),
                new Product("Batata Frita - Porção","Petiscos",26.90m,0m),
                new Product("Sanduíche de Picanha","Lanches da Madrugada",39.90m,0m),
                new Product("Sanduíche de Costela","Lanches da Madrugada",34.90m,0m),
                new Product("Batata Brasa","Lanches da Madrugada",39.90m,0m),
                new Product("Água sem Gás","Bebidas",5m,0m),
                new Product("Água com Gás","Bebidas",7m,0m),
                new Product("Refrigerante Lata","Bebidas",8m,0m),
                new Product("Refrigerante 1,5 L","Bebidas",16m,0m),
                new Product("Suco Natural 500 ml","Bebidas",15m,0m),
                new Product("Suco de Laranja 500 ml","Bebidas",22m,0m),
                new Product("Heineken Lata","Bebidas",14m,0m),
                new Product("Amstel Lata","Bebidas",10m,0m),
                new Product("Original 600 ml","Bebidas",18.50m,10.8m),
                new Product("Heineken 600 ml","Bebidas",18.50m,11.4m),
                new Product("Caipirinha","Bebidas",28m,0m),
                new Product("Caipivodka","Bebidas",32m,0m),
                new Product("Campari","Bebidas",10m,0m),
                new Product("Whisky","Bebidas",15m,0m),
                new Product("We Mix","Bebidas",12m,0m),
                new Product("Pink Moon - Chopp de Vinho","Bebidas",15m,0m),
                new Product("Stella Gold","Bebidas",15m,0m),
                new Product("Chopp Heineken 300 ml","Chopp Heineken",15m,0m),
                new Product("Chopp Heineken 500 ml","Chopp Heineken",20m,0m),
                new Product("Gin Red Berry","Drinks",43.50m,0m),
                new Product("Caip Berry Premium","Drinks",40.99m,0m),
                new Product("Frozen Berry","Drinks",38m,0m)
            };
        }

        public static bool ProductIsFood(Product product)
        {
            if (product == null) return true;
            return CategoryIsFood(product.Category);
        }

        public static bool CategoryIsFood(string category)
        {
            string value = category ?? "";
            return value != "Bebidas" && value != "Chopp Heineken" && value != "Drinks";
        }

        public static bool LineIsFood(OrderLine line)
        {
            return line != null && CategoryIsFood(line.Category);
        }

        public static bool OrderHasPendingFood(Order order)
        {
            return order != null && order.Lines != null && order.Lines.Any(x => LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt));
        }

        public static void RefreshKitchenStartedAt(Order order)
        {
            if (order == null || order.Lines == null) return;
            List<OrderLine> pending = order.Lines.Where(x => LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt)).ToList();
            if (pending.Count == 0) { order.KitchenStartedAt = null; return; }
            DateTime oldest = DateTime.MaxValue; string oldestValue = null;
            foreach (OrderLine line in pending)
            {
                DateTime parsed;
                if (DateTime.TryParse(line.CreatedAt, null, DateTimeStyles.RoundtripKind, out parsed) && parsed.ToUniversalTime() < oldest)
                {
                    oldest = parsed.ToUniversalTime(); oldestValue = line.CreatedAt;
                }
            }
            order.KitchenStartedAt = String.IsNullOrWhiteSpace(oldestValue) ? order.CreatedAt : oldestValue;
        }

        public static bool OrderHasFood(IEnumerable<Product> products, Order order)
        {
            if (order == null || order.Lines == null) return false;
            List<Product> catalog = products == null ? new List<Product>() : products.ToList();
            return order.Lines.Any(delegate(OrderLine line) {
                Product product = catalog.FirstOrDefault(x => x.Name == line.Name);
                return product == null ? LineIsFood(line) : ProductIsFood(product);
            });
        }
    }

    public static class ErpSyncClient
    {
        private const string BaseUrl = "https://hmdwsnjaijhccsmrvgas.supabase.co";
        private const string PublishableKey = "sb_publishable_4NWg-9pNsz0z2YRfRO4bUQ_X4ufB_kN";

        private static JavaScriptSerializer Serializer()
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            serializer.MaxJsonLength = Int32.MaxValue;
            return serializer;
        }

        private static WebClient Client(string accessToken)
        {
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            WebClient client = new WebClient();
            client.Encoding = Encoding.UTF8;
            client.Headers["apikey"] = PublishableKey;
            client.Headers["Content-Type"] = "application/json";
            if (!String.IsNullOrWhiteSpace(accessToken)) client.Headers["Authorization"] = "Bearer " + accessToken;
            return client;
        }

        public static ErpAuthSession SignIn(string email, string password)
        {
            using (WebClient client = Client(null))
            {
                string body = Serializer().Serialize(new { email = email.Trim(), password = password });
                string json = client.UploadString(BaseUrl + "/auth/v1/token?grant_type=password", "POST", body);
                return Serializer().Deserialize<ErpAuthSession>(json);
            }
        }

        public static ErpAuthSession Refresh(string refreshToken)
        {
            using (WebClient client = Client(null))
            {
                string body = Serializer().Serialize(new { refresh_token = refreshToken });
                string json = client.UploadString(BaseUrl + "/auth/v1/token?grant_type=refresh_token", "POST", body);
                return Serializer().Deserialize<ErpAuthSession>(json);
            }
        }

        public static T Get<T>(string path, string accessToken)
        {
            using (WebClient client = Client(accessToken))
            {
                string json = client.DownloadString(BaseUrl + path);
                return Serializer().Deserialize<T>(json);
            }
        }

        public static T Send<T>(string path, string method, object payload, string accessToken, string prefer)
        {
            using (WebClient client = Client(accessToken))
            {
                if (!String.IsNullOrWhiteSpace(prefer)) client.Headers["Prefer"] = prefer;
                string body = Serializer().Serialize(payload);
                string json = client.UploadString(BaseUrl + path, method, body);
                if (String.IsNullOrWhiteSpace(json)) return default(T);
                return Serializer().Deserialize<T>(json);
            }
        }

        public static void Delete(string path, string accessToken)
        {
            using (WebClient client = Client(accessToken)) client.UploadString(BaseUrl + path, "DELETE", "");
        }
    }

    public static class Theme
    {
        public static readonly Color Brown = Color.FromArgb(43, 33, 29);
        public static readonly Color Orange = Color.FromArgb(239, 108, 46);
        public static readonly Color Paper = Color.FromArgb(250, 248, 245);
        public static readonly Color Muted = Color.FromArgb(116, 107, 101);

        public static Button Button(string text, EventHandler click, bool primary)
        {
            Button button = new Button();
            button.Text = text; button.Height = 38; button.Width = 145; button.FlatStyle = FlatStyle.Flat;
            button.FlatAppearance.BorderSize = primary ? 0 : 1;
            button.BackColor = primary ? Orange : Color.White;
            button.ForeColor = primary ? Color.White : Brown;
            button.Font = new Font("Segoe UI", 9, FontStyle.Bold);
            button.Margin = new Padding(4);
            button.Click += click;
            return button;
        }

        public static Label Label(string text, float size, FontStyle style, Color color)
        {
            return new Label { Text = text, AutoSize = true, Font = new Font("Segoe UI", size, style), ForeColor = color, Margin = new Padding(3, 3, 3, 8) };
        }
    }

    public class MobileServer
    {
        private HttpListener listener;
        private Thread thread;
        private Func<string, string, string, string, string> apiHandler;
        public string ErrorMessage { get; private set; }
        public bool Running { get { return listener != null && listener.IsListening; } }

        public MobileServer(Func<string, string, string, string, string> handler) { apiHandler = handler; }

        public void Start()
        {
            try
            {
                listener = new HttpListener(); listener.Prefixes.Add("http://+:8787/"); listener.Start();
                thread = new Thread(ListenLoop); thread.IsBackground = true; thread.Start();
            }
            catch (Exception ex) { ErrorMessage = ex.Message; }
        }

        public void Stop()
        {
            try { if (listener != null) listener.Stop(); } catch { }
        }

        private void ListenLoop()
        {
            while (Running)
            {
                try { HttpListenerContext context = listener.GetContext(); ThreadPool.QueueUserWorkItem(delegate { Process(context); }); }
                catch { if (!Running) return; }
            }
        }

        private void Process(HttpListenerContext context)
        {
            try
            {
                string path = context.Request.Url.AbsolutePath;
                if (path == "/" || path == "/index.html") { Write(context, MobileHtml(), "text/html; charset=utf-8", 200); return; }
                if (path == "/manifest.json") { Write(context, "{\"name\":\"Ambrosia Atendimento\",\"short_name\":\"Ambrosia\",\"start_url\":\"/\",\"display\":\"standalone\",\"background_color\":\"#f7f3ec\",\"theme_color\":\"#005b43\",\"icons\":[{\"src\":\"/logo.png\",\"sizes\":\"512x512\",\"type\":\"image/png\",\"purpose\":\"any maskable\"}]}", "application/manifest+json; charset=utf-8", 200); return; }
                if (path == "/logo.png")
                {
                    string logoPath = Path.Combine(StateStore.DataFolder, "NativoSistemas.png");
                    if (File.Exists(logoPath)) { WriteBytes(context, File.ReadAllBytes(logoPath), "image/png", 200); return; }
                }
                if (!path.StartsWith("/api/")) { Write(context, "Nao encontrado", "text/plain", 404); return; }
                string body = ""; using (StreamReader reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) body = reader.ReadToEnd();
                string token = context.Request.Headers["X-Token"] ?? "";
                string result = apiHandler(path, context.Request.HttpMethod, body, token);
                Write(context, result, "application/json; charset=utf-8", 200);
            }
            catch (Exception ex) { Write(context, "{\"ok\":false,\"error\":\"" + Safe(ex.Message) + "\"}", "application/json; charset=utf-8", 500); }
        }

        private static string Safe(string value) { return (value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ").Replace("\n", " "); }
        private static void Write(HttpListenerContext context, string text, string contentType, int status)
        {
            try { byte[] bytes = Encoding.UTF8.GetBytes(text); context.Response.StatusCode = status; context.Response.ContentType = contentType; context.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate"; context.Response.ContentLength64 = bytes.Length; context.Response.OutputStream.Write(bytes, 0, bytes.Length); context.Response.OutputStream.Close(); } catch { }
        }

        private static void WriteBytes(HttpListenerContext context, byte[] bytes, string contentType, int status)
        {
            try { context.Response.StatusCode = status; context.Response.ContentType = contentType; context.Response.ContentLength64 = bytes.Length; context.Response.OutputStream.Write(bytes, 0, bytes.Length); context.Response.OutputStream.Close(); } catch { }
        }

        private static string MobileHtml()
        {
            return @"<!doctype html><html lang=""pt-BR""><head><meta charset=""utf-8""><meta name=""viewport"" content=""width=device-width,initial-scale=1,viewport-fit=cover""><meta name=""theme-color"" content=""#005b43""><link rel=""manifest"" href=""/manifest.json""><link rel=""icon"" href=""/logo.png""><link rel=""apple-touch-icon"" href=""/logo.png""><meta name=""apple-mobile-web-app-capable"" content=""yes""><meta name=""apple-mobile-web-app-title"" content=""Ambrosia""><title>Ambrosia Atendimento</title><style>
*{box-sizing:border-box}body{margin:0;font-family:Arial,sans-serif;background:#f7f3ec;color:#2d211c}header{background:#005b43;color:white;padding:16px;position:sticky;top:0;z-index:2}h1{font-size:20px;margin:0}.wrap{padding:14px;max-width:900px;margin:auto}.card{background:white;border-radius:16px;padding:16px;margin-bottom:14px;box-shadow:0 3px 14px #0001}.login{max-width:430px;margin:40px auto}input,button{font:inherit}input{width:100%;padding:13px;border:1px solid #ddd;border-radius:10px;margin:6px 0 12px}button{border:0;border-radius:10px;padding:12px 14px;font-weight:bold;background:#ff6f00;color:white}.secondary{background:#005b43}.gold{background:#cd9112}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.table{min-height:92px;background:#fafef2;color:#005b43;border:2px solid #5bb124}.busy{background:#007848;color:white;border-color:#007848}.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}.product{display:flex;justify-content:space-between;align-items:center;padding:11px 0;border-bottom:1px solid #eee}.line{display:flex;justify-content:space-between;gap:8px;padding:10px 0;border-bottom:1px solid #eee}.hide{display:none}.msg{color:#b12b20;font-weight:bold;margin:8px 0}@media(min-width:650px){.grid{grid-template-columns:repeat(6,1fr)}}
</style></head><body><header><h1>NS · AMBROSIA ATENDIMENTO</h1></header><div class=""wrap""><section id=""login"" class=""card login""><h2>Entrar</h2><input id=""user"" placeholder=""Usuário""><input id=""pass"" type=""password"" placeholder=""Senha""><div class=""toolbar""><button onclick=""login()"">ENTRAR</button><button class=""secondary"" onclick=""installShortcut()"">CRIAR ATALHO COM A LOGO</button></div><div id=""loginMsg"" class=""msg""></div></section><section id=""app"" class=""hide""><div class=""toolbar""><button class=""secondary"" onclick=""loadState()"">ATUALIZAR</button><button class=""gold"" onclick=""installShortcut()"">CRIAR ATALHO</button><button class=""gold"" onclick=""logout()"">SAIR</button></div><div id=""tables"" class=""grid""></div><div id=""service"" class=""card hide""><h2 id=""tableTitle""></h2><div class=""toolbar""><button class=""secondary"" onclick=""transfer()"">TRANSFERIR MESA</button><button class=""gold"" onclick=""partial()"">IMPRIMIR PARCIAL</button></div><h3>Itens</h3><div id=""lines""></div><h3>Lançar produto</h3><input id=""search"" placeholder=""Pesquisar produto"" oninput=""renderProducts()""><div id=""products""></div></div><div id=""msg"" class=""msg""></div></section></div><script>
let token='',data=null,current=0,deferredInstall=null;const el=id=>document.getElementById(id);window.addEventListener('beforeinstallprompt',e=>{e.preventDefault();deferredInstall=e});async function installShortcut(){if(deferredInstall){deferredInstall.prompt();await deferredInstall.userChoice;deferredInstall=null;return}let ios=/iphone|ipad|ipod/i.test(navigator.userAgent);alert(ios?'No iPhone: toque no botão Compartilhar e depois em Adicionar à Tela de Início. A logo Ambrosia será usada no atalho.':'No Android: abra o menu ⋮ do Chrome e toque em Adicionar à tela inicial ou Instalar aplicativo. A logo Ambrosia será usada no atalho.')}async function api(path,body){try{let r=await fetch('/api/'+path,{method:body?'POST':'GET',cache:'no-store',headers:{'Content-Type':'application/json','X-Token':token},body:body?JSON.stringify(body):null});return await r.json()}catch(e){return{ok:false,error:'Sem conexão com o computador. Confirme o Wi-Fi e mantenha o Ambrosia aberto.'}}}async function login(){let box=el('loginMsg');box.textContent='Entrando...';let r=await api('login',{user:el('user').value,password:el('pass').value});if(!r.ok){box.textContent=r.error;return}token=r.token;el('login').classList.add('hide');el('app').classList.remove('hide');await loadState()}function logout(){token='';location.reload()}async function loadState(){let r=await api('state');if(!r.ok){el('msg').textContent=r.error;return}data=r;renderTables();if(current)openTable(current)}function renderTables(){let area=el('tables');area.innerHTML='';for(let i=1;i<=data.tableCount;i++){let o=data.orders.find(x=>x.table==i),b=document.createElement('button');b.className='table '+(o?'busy':'');b.innerHTML='<b>MESA '+String(i).padStart(2,'0')+'</b><br>'+(o?o.total:'LIVRE');b.onclick=()=>openTable(i);area.appendChild(b)}}function openTable(n){current=n;el('service').classList.remove('hide');let o=data.orders.find(x=>x.table==n);el('tableTitle').textContent='MESA '+String(n).padStart(2,'0')+(o?' · '+o.total:' · LIVRE');el('lines').innerHTML=o?o.lines.map(x=>'<div class=""line""><span>'+x.qty+'x '+x.name+'</span><button onclick=""cancelItem(\''+x.id+'\')"">CANCELAR</button></div>').join(''):'Nenhum item lançado.';renderProducts();el('service').scrollIntoView({behavior:'smooth'})}function renderProducts(){if(!data)return;let q=(el('search').value||'').toLowerCase();el('products').innerHTML=data.products.filter(x=>x.name.toLowerCase().includes(q)).slice(0,40).map(x=>'<div class=""product""><span><b>'+x.name+'</b><br>'+x.category+' · '+x.price+'</span><button onclick=""addProduct(\''+encodeURIComponent(x.name)+'\')"">+ LANÇAR</button></div>').join('')}async function addProduct(name){let qty=prompt('Quantidade','1');if(!qty)return;let r=await api('add',{table:current,product:decodeURIComponent(name),quantity:qty});el('msg').textContent=r.ok?'Produto lançado e enviado para impressão.':r.error;await loadState()}async function cancelItem(id){let reason=prompt('Motivo obrigatório do cancelamento');if(!reason)return;let qty=prompt('Quantidade a cancelar','1');let r=await api('cancel',{table:current,lineId:id,quantity:qty,reason});el('msg').textContent=r.ok?'Produto cancelado.':r.error;await loadState()}async function transfer(){let target=prompt('Transferir para qual mesa?');if(!target)return;let r=await api('transfer',{table:current,target});el('msg').textContent=r.ok?'Mesa transferida.':r.error;if(r.ok)current=Number(target);await loadState()}async function partial(){let r=await api('partial',{table:current});el('msg').textContent=r.ok?'Parcial enviada para a impressora.':r.error}
</script></body></html>";
        }
    }

    public class MainForm : Form
    {
        private AppState state;
        private Panel content;
        private Label pageTitle;
        private System.Windows.Forms.Timer liveClock;
        private Dictionary<int, Button> tableClockButtons = new Dictionary<int, Button>();
        private DataGridView ordersClockGrid;
        private NotifyIcon kitchenAlerts;
        private HashSet<string> alertedKitchenLines = new HashSet<string>();
        private Dictionary<int, DateTime> latePrintRetryAt = new Dictionary<int, DateTime>();
        private string currentUserRole;
        private string currentUserName;
        private MobileServer mobileServer;
        private Dictionary<string, string> mobileSessions = new Dictionary<string, string>();

        private void ApplyApplicationIcon()
        {
            try
            {
                string installedIcon = Path.Combine(StateStore.DataFolder, "NativoSistemas.ico");
                Icon = File.Exists(installedIcon) ? new Icon(installedIcon) : Icon.ExtractAssociatedIcon(Application.ExecutablePath);
            }
            catch { }
        }

        public MainForm(string userRole, string userName, bool openCashAfterLogin)
        {
            state = StateStore.Load();
            currentUserRole = String.Equals(userRole, "admin", StringComparison.OrdinalIgnoreCase) ? "admin" : (String.Equals(userRole, "caixa", StringComparison.OrdinalIgnoreCase) ? "caixa" : "atendimento");
            currentUserName = String.IsNullOrWhiteSpace(userName) ? "Usuário" : userName;
            Text = state.RestaurantName + " — Sistema Nativo 2.8.4";
            Width = 1280; Height = 800; MinimumSize = new Size(1060, 680);
            StartPosition = FormStartPosition.CenterScreen; BackColor = Theme.Paper;
            Font = new Font("Segoe UI", 9);
            ApplyApplicationIcon();
            BuildShell();
            mobileServer = new MobileServer(HandleMobileApi); mobileServer.Start();
            kitchenAlerts = new NotifyIcon { Icon = SystemIcons.Warning, Visible = true, Text = "Ambrosia — alertas da cozinha" };
            liveClock = new System.Windows.Forms.Timer { Interval = 1000 };
            liveClock.Tick += delegate { RefreshLiveClocks(); CheckKitchenAlerts(); };
            liveClock.Start();
            FormClosed += delegate { liveClock.Stop(); liveClock.Dispose(); kitchenAlerts.Visible = false; kitchenAlerts.Dispose(); if (mobileServer != null) mobileServer.Stop(); };
            if (openCashAfterLogin || currentUserRole == "caixa") ShowCash(); else ShowDashboard();
        }

        private void BuildShell()
        {
            Panel sidebar = new Panel { Dock = DockStyle.Left, Width = 250, BackColor = Theme.Brown };
            PictureBox brand = new PictureBox { Dock = DockStyle.Top, Height = 125, SizeMode = PictureBoxSizeMode.Zoom, BackColor = Theme.Brown, Image = StateStore.LoadBrandImage(), Padding = new Padding(18, 6, 18, 6) };
            sidebar.Controls.Add(brand);

            FlowLayoutPanel nav = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown, WrapContents = false, AutoScroll = true, Padding = new Padding(14, 14, 14, 0) };
            nav.Controls.Add(NavButton("Início operacional", delegate { ShowDashboard(); }));
            if (currentUserRole != "caixa")
            {
                nav.Controls.Add(NavButton("Mesas", delegate { ShowTables(); }));
                nav.Controls.Add(NavButton("Pedidos e comandas", delegate { ShowOrders(); }));
            }
            nav.Controls.Add(NavButton("Caixa", delegate { ShowCash(); }));
            if (currentUserRole == "admin")
            {
                nav.Controls.Add(NavButton("Sincronizar", delegate { SyncFromErp(); }));
                nav.Controls.Add(NavButton("Configurações", delegate { ShowSettings(); }));
            }
            sidebar.Controls.Add(nav); nav.BringToFront();

            Panel header = new Panel { Dock = DockStyle.Top, Height = 82, BackColor = Color.White, Padding = new Padding(26, 20, 26, 10) };
            pageTitle = new Label { AutoSize = true, Font = new Font("Georgia", 21, FontStyle.Bold), ForeColor = Theme.Brown, Location = new Point(26, 25) };
            string roleLabel = currentUserRole == "admin" ? "ADMINISTRADOR" : (currentUserRole == "caixa" ? "CAIXA" : "ATENDIMENTO");
            Label mode = new Label { Text = roleLabel + " · " + currentUserName + " · NATIVO V2.8.4", AutoSize = true, ForeColor = Theme.Orange, Font = new Font("Segoe UI", 8, FontStyle.Bold), Anchor = AnchorStyles.Top | AnchorStyles.Right };
            mode.Location = new Point(720, 32);
            header.Controls.Add(pageTitle); header.Controls.Add(mode);

            content = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Paper, Padding = new Padding(28), AutoScroll = true };
            Controls.Add(content); Controls.Add(header); Controls.Add(sidebar);
        }

        private Button NavButton(string text, EventHandler click)
        {
            Button button = new Button { Text = text.ToUpperInvariant(), Width = 220, Height = 48, FlatStyle = FlatStyle.Flat, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(13, 0, 0, 0), BackColor = Theme.Brown, ForeColor = Color.FromArgb(220, 210, 204), Font = new Font("Arial", 11, FontStyle.Bold), Margin = new Padding(0, 2, 0, 2), Cursor = Cursors.Hand };
            button.FlatAppearance.BorderSize = 0;
            button.MouseEnter += delegate { button.BackColor = Theme.Orange; button.ForeColor = Color.White; };
            button.MouseLeave += delegate { button.BackColor = Theme.Brown; button.ForeColor = Color.FromArgb(220, 210, 204); };
            button.Click += click;
            return button;
        }

        private FlowLayoutPanel StartPage(string title, string subtitle)
        {
            tableClockButtons.Clear(); ordersClockGrid = null;
            content.Controls.Clear(); pageTitle.Text = title;
            FlowLayoutPanel page = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown, WrapContents = false, AutoScroll = true, BackColor = Theme.Paper };
            Label sub = Theme.Label(subtitle, 9, FontStyle.Regular, Theme.Muted); sub.MaximumSize = new Size(850, 0);
            page.Controls.Add(sub); content.Controls.Add(page); return page;
        }

        private Panel Stat(string label, string value, string note)
        {
            Panel card = new Panel { Width = 215, Height = 120, BackColor = Color.White, Margin = new Padding(0, 8, 12, 8), Padding = new Padding(17) };
            Label top = Theme.Label(label, 8, FontStyle.Bold, Theme.Muted); top.Location = new Point(17, 16);
            Label number = Theme.Label(value, 19, FontStyle.Bold, Theme.Brown); number.Location = new Point(17, 47);
            Label bottom = Theme.Label(note, 8, FontStyle.Regular, Theme.Muted); bottom.Location = new Point(17, 89);
            card.Controls.Add(top); card.Controls.Add(number); card.Controls.Add(bottom); return card;
        }

        private Button HomeAction(string symbol, string title, string subtitle, Color background, EventHandler click)
        {
            Button button = new Button {
                Text = symbol + "\r\n" + title.ToUpperInvariant() + "\r\n" + subtitle,
                Dock = DockStyle.Fill, Margin = new Padding(8), Padding = new Padding(8),
                FlatStyle = FlatStyle.Flat, BackColor = background, ForeColor = Color.White,
                Font = new Font("Segoe UI", 12, FontStyle.Bold), TextAlign = ContentAlignment.MiddleCenter,
                Cursor = Cursors.Hand
            };
            button.FlatAppearance.BorderSize = 0; button.Click += click; return button;
        }

        private string MobileUrl()
        {
            try
            {
                IPAddress address = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(x));
                return address == null ? "http://IP-DO-COMPUTADOR:8787" : "http://" + address + ":8787";
            }
            catch { return "http://IP-DO-COMPUTADOR:8787"; }
        }

        private void ShowMobileAccess()
        {
            FlowLayoutPanel page = StartPage("Aplicativo celular", "Atendimento pelo iPhone ou Android conectado à mesma rede Wi-Fi do computador.");
            string url = MobileUrl();
            Panel card = new Panel { Width = 900, Height = 250, BackColor = Color.White, Padding = new Padding(24), Margin = new Padding(0, 14, 0, 14) };
            Label status = Theme.Label(mobileServer != null && mobileServer.Running ? "● SERVIDOR MÓVEL ATIVO" : "● SERVIDOR MÓVEL NÃO INICIOU", 11, FontStyle.Bold, mobileServer != null && mobileServer.Running ? Color.FromArgb(0, 120, 72) : Color.Firebrick); status.Location = new Point(24, 22); card.Controls.Add(status);
            Label address = Theme.Label(url, 18, FontStyle.Bold, Theme.Orange); address.Location = new Point(24, 62); card.Controls.Add(address);
            Label help = Theme.Label("1. Conecte o celular à mesma rede Wi-Fi.\r\n2. Abra Safari ou Chrome e digite o endereço acima.\r\n3. Entre com um usuário Administrador ou Atendimento.\r\n4. No iPhone/Android, use Adicionar à Tela de Início para criar o ícone.", 10, FontStyle.Bold, Theme.Brown); help.Location = new Point(24, 105); help.MaximumSize = new Size(820, 0); card.Controls.Add(help);
            Button copy = Theme.Button("Copiar endereço", delegate { Clipboard.SetText(url); Alert("Endereço copiado."); }, true); copy.Location = new Point(650, 55); copy.Width = 170; card.Controls.Add(copy);
            page.Controls.Add(card);
            if (mobileServer != null && !mobileServer.Running) page.Controls.Add(Theme.Label("Erro: " + mobileServer.ErrorMessage + "\r\nExecute o instalador como Administrador para liberar a porta 8787 no Windows.", 9, FontStyle.Bold, Color.Firebrick));
            page.Controls.Add(Theme.Label("FUNÇÕES DISPONÍVEIS NO CELULAR", 10, FontStyle.Bold, Theme.Orange));
            page.Controls.Add(Theme.Label("Mesas • Lançar produtos • Cancelar produtos • Transferir mesa • Imprimir parcial", 10, FontStyle.Bold, Theme.Brown));
        }

        private string HandleMobileApi(string path, string method, string body, string token)
        {
            if (InvokeRequired) return (string)Invoke(new Func<string, string, string, string, string>(HandleMobileApi), path, method, body, token);
            JavaScriptSerializer json = new JavaScriptSerializer(); json.MaxJsonLength = Int32.MaxValue;
            try
            {
                Dictionary<string, object> request = String.IsNullOrWhiteSpace(body) ? new Dictionary<string, object>() : json.Deserialize<Dictionary<string, object>>(body);
                if (path == "/api/login")
                {
                    string username = request.ContainsKey("user") ? Convert.ToString(request["user"]).Trim() : ""; string password = request.ContainsKey("password") ? Convert.ToString(request["password"]) : "";
                    AppUser user = state.AppUsers.FirstOrDefault(x => x.Active && String.Equals(x.Username, username, StringComparison.OrdinalIgnoreCase));
                    if (user == null || StateStore.HashPassword(password) != user.PasswordHash) return json.Serialize(new { ok = false, error = "Usuário ou senha incorretos." });
                    if (String.Equals(user.Role, "caixa", StringComparison.OrdinalIgnoreCase)) return json.Serialize(new { ok = false, error = "O perfil Caixa não possui acesso ao atendimento móvel." });
                    string session = Guid.NewGuid().ToString("N"); mobileSessions[session] = user.Role; return json.Serialize(new { ok = true, token = session, name = user.DisplayName });
                }
                if (String.IsNullOrWhiteSpace(token) || !mobileSessions.ContainsKey(token)) return json.Serialize(new { ok = false, error = "Sessão inválida. Entre novamente." });
                if (path == "/api/state")
                {
                    var orders = state.Orders.Where(IsOpenOrder).Select(x => new { table = TableNumberFromLabel(x.Label), total = x.Total.ToString("C2"), lines = x.Lines.Select(l => new { id = l.LineId, name = l.Name, qty = l.Quantity.ToString("0.##") }).ToList() }).Where(x => x.table > 0).ToList();
                    var products = state.Products.Where(x => x.Active).OrderBy(x => x.Category).ThenBy(x => x.Name).Select(x => new { name = x.Name, category = x.Category, price = x.Price.ToString("C2") }).ToList();
                    return json.Serialize(new { ok = true, tableCount = state.TableCount, orders = orders, products = products });
                }
                int table = request.ContainsKey("table") ? Convert.ToInt32(request["table"]) : 0;
                if (table < 1 || table > state.TableCount) return json.Serialize(new { ok = false, error = "Mesa inválida." });
                Order order = ActiveOrderForTable(table);
                if (path == "/api/add")
                {
                    string productName = request.ContainsKey("product") ? Convert.ToString(request["product"]) : ""; decimal quantity = request.ContainsKey("quantity") ? Convert.ToDecimal(request["quantity"], CultureInfo.InvariantCulture) : 1m;
                    Product product = state.Products.FirstOrDefault(x => x.Active && String.Equals(x.Name, productName, StringComparison.OrdinalIgnoreCase)); if (product == null) return json.Serialize(new { ok = false, error = "Produto não encontrado." });
                    if (order == null) { order = new Order { Id = state.NextOrderId++, Label = "Mesa " + table.ToString("00"), Customer = "Atendimento móvel", Status = "Aberto", CreatedAt = DateTime.Now.ToString("o"), Lines = new List<OrderLine>() }; PrepareNewOrder(order); state.Orders.Add(order); }
                    AddItemToOrder(order, product, quantity); return json.Serialize(new { ok = true });
                }
                if (order == null) return json.Serialize(new { ok = false, error = "A mesa está livre." });
                if (path == "/api/cancel")
                {
                    string lineId = request.ContainsKey("lineId") ? Convert.ToString(request["lineId"]) : ""; string reason = request.ContainsKey("reason") ? Convert.ToString(request["reason"]) : ""; decimal quantity = request.ContainsKey("quantity") ? Convert.ToDecimal(request["quantity"], CultureInfo.InvariantCulture) : 1m;
                    OrderLine line = order.Lines.FirstOrDefault(x => x.LineId == lineId); if (line == null) return json.Serialize(new { ok = false, error = "Item não encontrado." });
                    bool done = CancelOrderItem(order, line, quantity, reason); return json.Serialize(new { ok = done, error = done ? "" : "Não foi possível cancelar o produto." });
                }
                if (path == "/api/transfer")
                {
                    int target = request.ContainsKey("target") ? Convert.ToInt32(request["target"]) : 0; if (target < 1 || target > state.TableCount) return json.Serialize(new { ok = false, error = "Mesa de destino inválida." }); if (ActiveOrderForTable(target) != null) return json.Serialize(new { ok = false, error = "A mesa de destino está ocupada." }); order.Label = "Mesa " + target.ToString("00"); Save(); return json.Serialize(new { ok = true });
                }
                if (path == "/api/partial") { Print(order, false, true); return json.Serialize(new { ok = true }); }
                return json.Serialize(new { ok = false, error = "Operação não encontrada." });
            }
            catch (Exception ex) { return json.Serialize(new { ok = false, error = ex.Message }); }
        }

        private bool IsToday(string value)
        {
            DateTime parsed; return DateTime.TryParse(value, null, DateTimeStyles.RoundtripKind, out parsed) && parsed.ToLocalTime().Date == DateTime.Today;
        }

        private void Save() { StateStore.Save(state); }

        public static TimeSpan ElapsedSince(string createdAt)
        {
            DateTime parsed;
            if (!DateTime.TryParse(createdAt, null, DateTimeStyles.RoundtripKind, out parsed)) return TimeSpan.Zero;
            TimeSpan elapsed = DateTime.Now - parsed.ToLocalTime();
            return elapsed < TimeSpan.Zero ? TimeSpan.Zero : elapsed;
        }

        public static string ElapsedText(string createdAt)
        {
            TimeSpan elapsed = ElapsedSince(createdAt);
            int hours = Convert.ToInt32(Math.Floor(elapsed.TotalHours));
            return hours.ToString("00") + ":" + elapsed.Minutes.ToString("00") + ":" + elapsed.Seconds.ToString("00");
        }

        public static string ElapsedTextUntil(string createdAt, string finishedAt)
        {
            DateTime start; DateTime finish;
            if (!DateTime.TryParse(createdAt, null, DateTimeStyles.RoundtripKind, out start) || !DateTime.TryParse(finishedAt, null, DateTimeStyles.RoundtripKind, out finish)) return "00:00:00";
            TimeSpan elapsed = finish.ToLocalTime() - start.ToLocalTime();
            if (elapsed < TimeSpan.Zero) elapsed = TimeSpan.Zero;
            int hours = Convert.ToInt32(Math.Floor(elapsed.TotalHours));
            return hours.ToString("00") + ":" + elapsed.Minutes.ToString("00") + ":" + elapsed.Seconds.ToString("00");
        }

        public static Color KitchenColorFor(string createdAt)
        {
            double minutes = ElapsedSince(createdAt).TotalMinutes;
            if (minutes >= 25) return Color.Firebrick;
            if (minutes >= 15) return Color.FromArgb(205, 139, 35);
            return Color.FromArgb(45, 115, 87);
        }

        private Color ClockColor(Order order)
        {
            if (order == null || String.IsNullOrWhiteSpace(order.KitchenStartedAt)) return Theme.Brown;
            return KitchenColorFor(order.KitchenStartedAt);
        }

        private string OccupiedTableText(Order order, int tableNumber)
        {
            int count = order.Lines == null ? 0 : order.Lines.Sum(x => Convert.ToInt32(x.Quantity));
            string kitchenClock = String.IsNullOrWhiteSpace(order.KitchenStartedAt) ? "SEM COMIDA" : "COZINHA " + ElapsedText(order.KitchenStartedAt);
            return order.Status.ToUpperInvariant() + "\r\n" + tableNumber.ToString("00") + " · " + kitchenClock + "\r\n" +
                (String.IsNullOrWhiteSpace(order.Customer) ? "Cliente não informado" : order.Customer) + "\r\n" +
                count + (count == 1 ? " item · " : " itens · ") + order.Total.ToString("C2");
        }

        private void RefreshLiveClocks()
        {
            foreach (KeyValuePair<int, Button> entry in tableClockButtons.ToList())
            {
                if (entry.Value == null || entry.Value.IsDisposed) { tableClockButtons.Remove(entry.Key); continue; }
                Order order = state.Orders.FirstOrDefault(x => x.Id == entry.Key && IsOpenOrder(x)); if (order == null) continue;
                int tableNumber = TableNumberFromLabel(order.Label);
                entry.Value.Text = OccupiedTableText(order, tableNumber); entry.Value.BackColor = ClockColor(order);
            }
            if (ordersClockGrid == null || ordersClockGrid.IsDisposed) return;
            foreach (DataGridViewRow row in ordersClockGrid.Rows)
            {
                if (row.Tag == null) continue;
                Order order = state.Orders.FirstOrDefault(x => x.Id == Convert.ToInt32(row.Tag)); if (order == null) continue;
                row.Cells["Time"].Value = IsOpenOrder(order) ? (String.IsNullOrWhiteSpace(order.KitchenStartedAt) ? "—" : ElapsedText(order.KitchenStartedAt)) : (order.Status == "Pago" ? "Fechado" : "Cancelado");
                row.Cells["Time"].Style.ForeColor = IsOpenOrder(order) && !String.IsNullOrWhiteSpace(order.KitchenStartedAt) ? ClockColor(order) : Theme.Muted;
            }
        }

        private void CheckKitchenAlerts()
        {
            foreach (Order order in state.Orders.Where(IsOpenOrder))
            {
                if (order.Lines == null) continue;
                foreach (OrderLine line in order.Lines.Where(x => StateStore.LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt)))
                {
                    if (String.IsNullOrWhiteSpace(line.LineId) || ElapsedSince(line.CreatedAt).TotalMinutes < 25 || alertedKitchenLines.Contains(line.LineId)) continue;
                    alertedKitchenLines.Add(line.LineId);
                    string itemName = line.Name ?? "Comida";
                    if (itemName.Length > 36) itemName = itemName.Substring(0, 36) + "...";
                    kitchenAlerts.BalloonTipIcon = ToolTipIcon.Warning;
                    kitchenAlerts.BalloonTipTitle = "Pedido com 25 minutos";
                    kitchenAlerts.BalloonTipText = order.Label + " · " + itemName;
                    kitchenAlerts.ShowBalloonTip(10000);
                }

                if (order.LateNoticePrintedLineIds == null) order.LateNoticePrintedLineIds = new List<string>();
                List<OrderLine> lateLines = order.Lines
                    .Where(x => StateStore.LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt) &&
                        !String.IsNullOrWhiteSpace(x.LineId) && ElapsedSince(x.CreatedAt).TotalMinutes >= 30 &&
                        !order.LateNoticePrintedLineIds.Contains(x.LineId))
                    .OrderBy(x => x.CreatedAt)
                    .ToList();
                if (lateLines.Count == 0) continue;

                DateTime retryAt;
                if (latePrintRetryAt.TryGetValue(order.Id, out retryAt) && DateTime.Now < retryAt) continue;
                if (PrintLateOrderAlert(order, lateLines))
                {
                    foreach (OrderLine line in lateLines) order.LateNoticePrintedLineIds.Add(line.LineId);
                    latePrintRetryAt.Remove(order.Id);
                    Save();
                }
                else latePrintRetryAt[order.Id] = DateTime.Now.AddMinutes(2);
            }
        }

        private bool PrintLateOrderAlert(Order order, List<OrderLine> lateLines)
        {
            if (String.IsNullOrWhiteSpace(state.PrinterName))
            {
                kitchenAlerts.BalloonTipIcon = ToolTipIcon.Error;
                kitchenAlerts.BalloonTipTitle = "Pedido atrasado sem impressão";
                kitchenAlerts.BalloonTipText = order.Label + " passou de 30 minutos. Configure a impressora.";
                kitchenAlerts.ShowBalloonTip(10000);
                return false;
            }
            try
            {
                PrintDocument doc = new PrintDocument();
                doc.PrinterSettings.PrinterName = state.PrinterName;
                doc.PrintController = new StandardPrintController();
                doc.PrinterSettings.Copies = 1;
                int paperWidth = state.PaperWidth == 58 ? 228 : 315;
                doc.DefaultPageSettings.PaperSize = new PaperSize(state.PaperWidth + " mm", paperWidth, 750);
                doc.DefaultPageSettings.Margins = new Margins(8, 8, 5, 5);
                doc.PrintPage += delegate(object sender, PrintPageEventArgs e) {
                    float normalSize = ReceiptFontPointSize(false);
                    float boldSize = Math.Max(ReceiptFontPointSize(true), state.PaperWidth == 58 ? 10f : 13f);
                    Font normal = new Font(PrintFontFamily(), normalSize);
                    Font bold = new Font(PrintFontFamily(), boldSize, FontStyle.Bold);
                    float y = 8;
                    Action<string, Font> line = delegate(string text, Font font) {
                        e.Graphics.DrawString(text, font, Brushes.Black, 8, y);
                        y += font.GetHeight(e.Graphics) + 4;
                    };
                    line(state.RestaurantName.ToUpperInvariant(), bold);
                    line("********************************", normal);
                    line("PEDIDO ATRASADO", bold);
                    line("POR FAVOR, ADIANTE O PROCESSO", bold);
                    line("********************************", normal);
                    line(order.Label + "  #" + order.Id, bold);
                    line("Tempo: " + ElapsedText(lateLines.First().CreatedAt), bold);
                    line("Lancado: " + LateOrderTime(lateLines.First().CreatedAt), normal);
                    line("--------------------------------", normal);
                    foreach (OrderLine item in lateLines)
                    {
                        line(item.Quantity.ToString("0.##") + "x " + item.Name, bold);
                        line("Esperando: " + ElapsedText(item.CreatedAt), normal);
                    }
                    line("--------------------------------", normal);
                    line("IMPRESSO AUTOMATICAMENTE", normal);
                    line(DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"), normal);
                    e.HasMorePages = false;
                    normal.Dispose(); bold.Dispose();
                };
                doc.Print(); doc.Dispose();
                return true;
            }
            catch (Exception ex)
            {
                kitchenAlerts.BalloonTipIcon = ToolTipIcon.Error;
                kitchenAlerts.BalloonTipTitle = "Falha ao imprimir pedido atrasado";
                kitchenAlerts.BalloonTipText = order.Label + " · " + ex.Message;
                kitchenAlerts.ShowBalloonTip(10000);
                return false;
            }
        }

        private static string LateOrderTime(string createdAt)
        {
            DateTime parsed;
            return DateTime.TryParse(createdAt, null, DateTimeStyles.RoundtripKind, out parsed)
                ? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")
                : "Horário não informado";
        }

        private void ShowDashboard()
        {
            if (currentUserRole == "caixa") { ShowCash(); return; }
            FlowLayoutPanel page = StartPage("Início operacional", "Escolha uma operação para começar o atendimento.");
            List<Order> paid = state.Orders.Where(x => x.Status == "Pago" && IsToday(x.ClosedAt)).ToList();
            decimal sales = paid.Sum(x => x.Total);
            int open = state.Orders.Count(IsOpenOrder);
            TableLayoutPanel actions = new TableLayoutPanel { Width = 950, Height = 390, ColumnCount = 3, RowCount = 3, Margin = new Padding(0, 12, 0, 12) };
            actions.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 42f));
            actions.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30f));
            actions.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 28f));
            for (int row = 0; row < 3; row++) actions.RowStyles.Add(new RowStyle(SizeType.Percent, 33.333f));
            Button cash = HomeAction("▣", "Caixa", "Recebimentos e fechamento", Color.FromArgb(0, 91, 67), delegate { ShowCash(); });
            actions.Controls.Add(cash, 0, 0); actions.SetRowSpan(cash, 3);
            Button tables = HomeAction("▥", "Mesas", "Mapa de 1 a " + state.TableCount, Color.FromArgb(91, 177, 36), delegate { ShowTables(); });
            actions.Controls.Add(tables, 1, 0); actions.SetRowSpan(tables, 2);
            actions.Controls.Add(HomeAction("▤", "Comanda", "Novo atendimento", Color.FromArgb(230, 125, 20), delegate { NewOrder(); ShowDashboard(); }), 1, 2);
            actions.Controls.Add(HomeAction("▣", "Balcão", "Venda rápida", Color.FromArgb(255, 111, 0), delegate { NewCounterOrder(); ShowDashboard(); }), 2, 0);
            actions.Controls.Add(HomeAction("−", "Sangria", "Retirada do caixa", Color.FromArgb(0, 120, 72), delegate { RegisterCashWithdrawal(false); }), 2, 1);
            actions.Controls.Add(HomeAction("↻", "Sincronizar", currentUserRole == "admin" ? "Um clique · ERP online" : "Somente administrador", Color.FromArgb(205, 145, 18), delegate { if (currentUserRole == "admin") SyncFromErp(); else Alert("A sincronização está disponível somente para o Administrador."); }), 2, 2);
            page.Controls.Add(actions);
            string printerStatus = String.IsNullOrWhiteSpace(state.PrinterName)
                ? "⚠ Configure a impressora em Configurações → Impressão. Os lançamentos das mesas exigem uma impressora selecionada."
                : "✓ Impressão automática das mesas ativa em: " + state.PrinterName;
            page.Controls.Add(Theme.Label(printerStatus, 9, FontStyle.Bold, String.IsNullOrWhiteSpace(state.PrinterName) ? Color.Firebrick : Color.FromArgb(45, 115, 87)));
            FlowLayoutPanel stats = new FlowLayoutPanel { Width = 950, Height = 132, WrapContents = false };
            stats.Controls.Add(Stat("VENDAS HOJE", sales.ToString("C2"), paid.Count + " contas pagas"));
            stats.Controls.Add(Stat("ATENDIMENTOS ABERTOS", open.ToString(), "Mesas, balcão e comandas"));
            stats.Controls.Add(Stat("TICKET MÉDIO", paid.Count == 0 ? "R$ 0,00" : (sales / paid.Count).ToString("C2"), "Média das contas"));
            page.Controls.Add(stats);
        }

        private int TableNumberFromLabel(string value)
        {
            if (String.IsNullOrWhiteSpace(value)) return 0;
            string normalized = value.Trim();
            if (!normalized.StartsWith("Mesa", StringComparison.OrdinalIgnoreCase)) return 0;
            int result;
            if (!Int32.TryParse(normalized.Substring(4).Trim(), out result)) return 0;
            return result >= 1 && result <= state.TableCount ? result : 0;
        }

        private Order ActiveOrderForTable(int tableNumber)
        {
            return state.Orders.OrderByDescending(x => x.Id).FirstOrDefault(x => IsOpenOrder(x) && TableNumberFromLabel(x.Label) == tableNumber);
        }

        private bool IsOpenOrder(Order order)
        {
            return order != null && order.Status != "Pago" && order.Status != "Cancelado";
        }

        private InventoryItem FindInventoryForProduct(string productName)
        {
            if (String.IsNullOrWhiteSpace(productName)) return null;
            return state.Inventory
                .Where(x => !String.IsNullOrWhiteSpace(x.Name) && productName.IndexOf(x.Name, StringComparison.OrdinalIgnoreCase) >= 0)
                .OrderByDescending(x => x.Name.Length)
                .FirstOrDefault();
        }

        private decimal InventoryUsePerProduct(string productName, InventoryItem inventory)
        {
            if (inventory == null) return 0m;
            if (String.Equals(inventory.Unit, "un", StringComparison.OrdinalIgnoreCase)) return 1m;
            string normalized = (productName ?? "").Replace(',', '.');
            Match match;
            if (String.Equals(inventory.Unit, "kg", StringComparison.OrdinalIgnoreCase))
            {
                match = Regex.Match(normalized, @"([0-9]+(?:\.[0-9]+)?)\s*kg", RegexOptions.IgnoreCase);
                if (match.Success) return Decimal.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
                match = Regex.Match(normalized, @"([0-9]+(?:\.[0-9]+)?)\s*g", RegexOptions.IgnoreCase);
                if (match.Success) return Decimal.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture) / 1000m;
            }
            if (String.Equals(inventory.Unit, "L", StringComparison.OrdinalIgnoreCase))
            {
                match = Regex.Match(normalized, @"([0-9]+(?:\.[0-9]+)?)\s*l", RegexOptions.IgnoreCase);
                if (match.Success) return Decimal.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
                match = Regex.Match(normalized, @"([0-9]+(?:\.[0-9]+)?)\s*ml", RegexOptions.IgnoreCase);
                if (match.Success) return Decimal.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture) / 1000m;
            }
            return 0m;
        }

        private void DeductInventory(OrderLine line, Product product, decimal soldQuantity)
        {
            if (line == null || product == null || soldQuantity <= 0) return;
            InventoryItem inventory = String.IsNullOrWhiteSpace(line.InventoryName) ? FindInventoryForProduct(product.Name) : state.Inventory.FirstOrDefault(x => x.Name == line.InventoryName);
            if (inventory == null) return;
            decimal perUnit = line.InventoryQuantityPerUnit > 0 ? line.InventoryQuantityPerUnit : InventoryUsePerProduct(product.Name, inventory);
            if (perUnit <= 0) return;
            decimal used = perUnit * soldQuantity;
            decimal deducted = Math.Min(inventory.Quantity, used);
            line.InventoryName = inventory.Name; line.InventoryQuantityPerUnit = perUnit;
            line.InventoryDeductedQuantity += deducted;
            inventory.Quantity -= deducted;
        }

        private void RestoreInventory(OrderLine line, decimal cancelledQuantity)
        {
            if (line == null || cancelledQuantity <= 0 || line.InventoryDeductedQuantity <= 0 || line.InventoryQuantityPerUnit <= 0) return;
            InventoryItem inventory = state.Inventory.FirstOrDefault(x => x.Name == line.InventoryName); if (inventory == null) return;
            decimal restore = Math.Min(line.InventoryDeductedQuantity, cancelledQuantity * line.InventoryQuantityPerUnit);
            inventory.Quantity += restore; line.InventoryDeductedQuantity -= restore;
        }

        private void PrepareNewOrder(Order order)
        {
            if (order.Payments == null) order.Payments = new List<PaymentEntry>();
            if (order.Cancellations == null) order.Cancellations = new List<CancellationEntry>();
            if (order.Lines == null) order.Lines = new List<OrderLine>();
            if (String.IsNullOrWhiteSpace(order.CreatedAt)) order.CreatedAt = DateTime.Now.ToString("o");
            order.ServiceFeePercent = TableNumberFromLabel(order.Label) > 0 ? 10m : 0m;
            foreach (OrderLine line in order.Lines)
            {
                Product product = state.Products.FirstOrDefault(x => x.Name == line.Name);
                line.LineId = String.IsNullOrWhiteSpace(line.LineId) ? Guid.NewGuid().ToString("N") : line.LineId;
                line.Category = product == null ? "Outros" : product.Category;
                line.CreatedAt = order.CreatedAt;
                line.ServedAt = null;
                DeductInventory(line, product, line.Quantity);
            }
            StateStore.RefreshKitchenStartedAt(order);
        }

        private void ShowTables()
        {
            FlowLayoutPanel page = StartPage("Mesas 1 a " + state.TableCount, "Clique em uma mesa livre para abrir o atendimento ou em uma mesa ocupada para adicionar produtos.");
            int occupied = Enumerable.Range(1, state.TableCount).Count(x => ActiveOrderForTable(x) != null);
            Label summary = Theme.Label((state.TableCount - occupied) + " mesas livres     •     " + occupied + " mesas ocupadas     •     " + state.TableCount + " mesas configuradas", 10, FontStyle.Bold, Theme.Brown);
            summary.Margin = new Padding(3, 5, 3, 12); page.Controls.Add(summary);

            int columns = state.TableCount <= 20 ? 5 : (state.TableCount <= 42 ? 6 : 8);
            int rows = Convert.ToInt32(Math.Ceiling(state.TableCount / (double)columns));
            TableLayoutPanel room = new TableLayoutPanel { Width = 950, Height = Math.Max(420, rows * 115), ColumnCount = columns, RowCount = rows, BackColor = Theme.Paper, Margin = new Padding(0, 2, 0, 15) };
            for (int column = 0; column < columns; column++) room.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f / columns));
            for (int row = 0; row < rows; row++) room.RowStyles.Add(new RowStyle(SizeType.Absolute, 115));
            for (int index = 1; index <= state.TableCount; index++)
            {
                int tableNumber = index;
                Order order = ActiveOrderForTable(tableNumber);
                string tableLabel = "Mesa " + tableNumber.ToString("00");
                Button table = new Button { Dock = DockStyle.Fill, Margin = new Padding(6), FlatStyle = FlatStyle.Flat, TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Arial", 10, FontStyle.Bold), Cursor = Cursors.Hand };
                if (order == null)
                {
                    table.Text = "LIVRE\r\n\r\n" + tableNumber.ToString("00") + "\r\n\r\n+ Abrir mesa";
                    table.BackColor = Color.FromArgb(250, 255, 242); table.ForeColor = Color.FromArgb(0, 91, 67); table.FlatAppearance.BorderColor = Color.FromArgb(91, 177, 36); table.FlatAppearance.BorderSize = 2;
                    table.MouseEnter += delegate { table.BackColor = Color.FromArgb(232, 249, 204); };
                    table.MouseLeave += delegate { table.BackColor = Color.FromArgb(250, 255, 242); };
                    table.Click += delegate { OpenTable(tableNumber); ShowTables(); };
                }
                else
                {
                    int orderId = order.Id;
                    table.Text = OccupiedTableText(order, tableNumber);
                    table.BackColor = ClockColor(order); table.ForeColor = Color.White; table.FlatAppearance.BorderSize = 0;
                    tableClockButtons[orderId] = table;
                    table.Click += delegate { ManageTable(orderId); ShowTables(); };
                }
                room.Controls.Add(table, (index - 1) % columns, (index - 1) / columns);
            }
            page.Controls.Add(room);
            page.Controls.Add(Theme.Label("As mesas ocupadas também aparecem em Pedidos e comandas, Caixa e Relatórios.", 9, FontStyle.Regular, Theme.Muted));
        }

        private void OpenTable(int tableNumber)
        {
            if (ActiveOrderForTable(tableNumber) != null) { Alert("Esta mesa já está ocupada."); return; }
            string label = "Mesa " + tableNumber.ToString("00");
            using (NewOrderForm form = new NewOrderForm(state.Products, state.NextOrderId, label))
            {
                if (form.ShowDialog(this) == DialogResult.OK)
                {
                    PrepareNewOrder(form.CreatedOrder);
                    state.Orders.Add(form.CreatedOrder); state.NextOrderId++; Save();
                    PrintTableItems(form.CreatedOrder, form.CreatedOrder.Lines);
                }
            }
        }

        private void ManageTable(int orderId)
        {
            ManageOrder(orderId);
        }

        private void ManageOrder(int orderId)
        {
            Order order = state.Orders.FirstOrDefault(x => x.Id == orderId && IsOpenOrder(x));
            if (order == null) { Alert("Esta mesa já foi liberada."); return; }
            using (TableServiceForm form = new TableServiceForm(order, state.Products,
                delegate(Product product, decimal quantity) { AddItemToOrder(order, product, quantity); },
                delegate(OrderLine line, decimal quantity, string reason) { return CancelOrderItem(order, line, quantity, reason); },
                delegate(string reason) { return CancelOrder(order, reason); },
                delegate { MarkKitchenServed(order); },
                delegate { using (PartialAccountForm partial = new PartialAccountForm(order, delegate { Print(order, false, true); })) partial.ShowDialog(this); },
                delegate { Print(order, false, true); },
                delegate { return TransferTable(order); },
                delegate { ReceiveOrder(order.Id); return order.Status == "Pago"; }))
            {
                form.ShowDialog(this);
            }
        }

        private void AddItemToOrder(Order order, Product product, decimal quantity)
        {
            if (order == null || product == null || quantity <= 0 || !IsOpenOrder(order)) return;
            if (order.Lines == null) order.Lines = new List<OrderLine>();
            string stamp = DateTime.Now.ToString("o");
            OrderLine line = new OrderLine { LineId = Guid.NewGuid().ToString("N"), Name = product.Name, Category = product.Category, CreatedAt = stamp, ServedAt = null, Quantity = quantity, UnitPrice = product.Price, UnitCost = product.Cost };
            order.Lines.Add(line); DeductInventory(line, product, quantity);
            if (StateStore.ProductIsFood(product)) order.Status = "Na cozinha";
            StateStore.RefreshKitchenStartedAt(order); Save();
            if (TableNumberFromLabel(order.Label) > 0) PrintTableItems(order, new List<OrderLine> { line });
        }

        private bool TransferTable(Order order)
        {
            if (order == null || TableNumberFromLabel(order.Label) < 1 || !IsOpenOrder(order)) return false;
            int target;
            if (!Int32.TryParse(PromptForm.Ask("Transferir " + order.Label + " para qual mesa?", ""), out target) || target < 1 || target > state.TableCount) { Alert("Informe uma mesa entre 1 e " + state.TableCount + "."); return false; }
            if (ActiveOrderForTable(target) != null) { Alert("A Mesa " + target.ToString("00") + " já está ocupada."); return false; }
            string previous = order.Label; order.Label = "Mesa " + target.ToString("00"); Save();
            Alert(previous + " transferida para " + order.Label + " com sucesso."); return true;
        }

        private void PrintTableItems(Order order, List<OrderLine> newLines)
        {
            if (order == null || newLines == null || newLines.Count == 0) return;
            Order ticket = new Order {
                Id = order.Id, Label = order.Label, Customer = order.Customer,
                Status = "Novo lançamento", CreatedAt = DateTime.Now.ToString("o"),
                Lines = newLines.ToList(), Payments = new List<PaymentEntry>(),
                Cancellations = new List<CancellationEntry>(), ServiceFeePercent = 0m
            };
            Print(ticket, false, false);
        }

        private void MarkKitchenServed(Order order)
        {
            if (order == null || order.Lines == null || !IsOpenOrder(order)) return;
            List<OrderLine> pending = order.Lines.Where(x => StateStore.LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt)).ToList();
            if (pending.Count == 0) { Alert("Não há comida aguardando para marcar como servida."); return; }
            string stamp = DateTime.Now.ToString("o");
            foreach (OrderLine line in pending) line.ServedAt = stamp;
            order.Status = "Servido"; StateStore.RefreshKitchenStartedAt(order); Save();
        }

        private bool CancelOrderItem(Order order, OrderLine line, decimal quantity, string reason)
        {
            if (order == null || line == null || !IsOpenOrder(order)) return false;
            if (order.PaidAmount > 0) { Alert("Não é possível cancelar item depois de registrar pagamento parcial."); return false; }
            if (quantity <= 0 || quantity > line.Quantity || String.IsNullOrWhiteSpace(reason)) { Alert("Informe a quantidade e o motivo do cancelamento."); return false; }
            RestoreInventory(line, quantity);
            if (order.Cancellations == null) order.Cancellations = new List<CancellationEntry>();
            CancellationEntry cancellation = new CancellationEntry { Scope = "Item", ItemName = line.Name, Quantity = quantity, Reason = reason.Trim(), CreatedAt = DateTime.Now.ToString("o") };
            order.Cancellations.Add(cancellation);
            line.Quantity -= quantity; if (line.Quantity <= 0.0001m) order.Lines.Remove(line);
            StateStore.RefreshKitchenStartedAt(order);
            Save(); PrintCancellation(order, cancellation); return true;
        }

        private bool CancelOrder(Order order, string reason)
        {
            if (order == null || !IsOpenOrder(order)) return false;
            if (order.PaidAmount > 0) { Alert("Não é possível cancelar a mesa ou pedido depois de registrar pagamento parcial."); return false; }
            if (String.IsNullOrWhiteSpace(reason)) { Alert("O motivo do cancelamento é obrigatório."); return false; }
            foreach (OrderLine line in order.Lines.ToList()) RestoreInventory(line, line.Quantity);
            string stamp = DateTime.Now.ToString("o");
            if (order.Cancellations == null) order.Cancellations = new List<CancellationEntry>();
            CancellationEntry cancellation = new CancellationEntry { Scope = "Pedido", ItemName = order.Label, Quantity = 1, Reason = reason.Trim(), CreatedAt = stamp };
            order.Cancellations.Add(cancellation);
            foreach (OrderLine line in order.Lines.Where(x => StateStore.LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt))) line.ServedAt = stamp;
            order.Status = "Cancelado"; order.CancellationReason = reason.Trim(); order.CancelledAt = stamp; StateStore.RefreshKitchenStartedAt(order); Save(); PrintCancellation(order, cancellation); return true;
        }

        private DataGridView Grid()
        {
            DataGridView grid = new DataGridView { Width = 950, Height = 440, BackgroundColor = Color.White, BorderStyle = BorderStyle.None, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect, MultiSelect = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, RowHeadersVisible = false, Margin = new Padding(0, 12, 0, 10) };
            grid.EnableHeadersVisualStyles = false; grid.ColumnHeadersDefaultCellStyle.BackColor = Theme.Brown; grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.White; grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9, FontStyle.Bold); grid.RowTemplate.Height = 31;
            return grid;
        }

        private int SelectedId(DataGridView grid)
        {
            if (grid.SelectedRows.Count == 0 || grid.SelectedRows[0].Tag == null) return 0;
            return Convert.ToInt32(grid.SelectedRows[0].Tag);
        }

        private void ShowOrders()
        {
            FlowLayoutPanel page = StartPage("Pedidos e comandas", "Crie pedidos, envie para produção, receba e imprima sem navegador.");
            FlowLayoutPanel actions = new FlowLayoutPanel { Width = 950, Height = 95, WrapContents = true };
            DataGridView grid = Grid();
            grid.Columns.Add("Number", "Nº"); grid.Columns.Add("Label", "Mesa / comanda"); grid.Columns.Add("Customer", "Cliente"); grid.Columns.Add("Status", "Status"); grid.Columns.Add("Total", "Total"); grid.Columns.Add("Time", "Tempo comida");
            ordersClockGrid = grid;
            Action reload = delegate {
                grid.Rows.Clear();
                foreach (Order order in state.Orders.OrderByDescending(x => x.Id)) {
                    int row = grid.Rows.Add(order.Id, order.Label, order.Customer, order.Status, order.Total.ToString("C2"), IsOpenOrder(order) ? (String.IsNullOrWhiteSpace(order.KitchenStartedAt) ? "—" : ElapsedText(order.KitchenStartedAt)) : (order.Status == "Pago" ? "Fechado" : "Cancelado"));
                    grid.Rows[row].Tag = order.Id;
                    grid.Rows[row].Cells["Time"].Style.Font = new Font("Segoe UI", 9, FontStyle.Bold);
                    grid.Rows[row].Cells["Time"].Style.ForeColor = IsOpenOrder(order) && !String.IsNullOrWhiteSpace(order.KitchenStartedAt) ? ClockColor(order) : Theme.Muted;
                }
            };
            actions.Controls.Add(Theme.Button("+ Nova comanda", delegate { NewOrder(); ShowOrders(); }, true));
            actions.Controls.Add(Theme.Button("Abrir / itens", delegate { ManageOrder(SelectedId(grid)); reload(); }, false));
            actions.Controls.Add(Theme.Button("Enviar à cozinha", delegate {
                Order order = state.Orders.FirstOrDefault(x => x.Id == SelectedId(grid)); if (order == null) { Alert("Selecione uma comanda."); return; }
                if (!IsOpenOrder(order)) { Alert("Este pedido já foi finalizado ou cancelado."); return; }
                order.Status = "Na cozinha"; Save(); reload();
            }, false));
            actions.Controls.Add(Theme.Button("Ver parcial", delegate { ShowPartial(SelectedId(grid)); }, false));
            actions.Controls.Add(Theme.Button("Receber", delegate { ReceiveOrder(SelectedId(grid)); reload(); }, true));
            actions.Controls.Add(Theme.Button("Imprimir", delegate { PrintOrder(SelectedId(grid)); }, false));
            actions.Controls.Add(Theme.Button("Cancelar pedido", delegate {
                Order order = state.Orders.FirstOrDefault(x => x.Id == SelectedId(grid)); if (order == null) { Alert("Selecione uma comanda."); return; }
                using (CancelOrderForm cancel = new CancelOrderForm(order.Label)) if (cancel.ShowDialog(this) == DialogResult.OK && CancelOrder(order, cancel.Reason)) { Alert("Pedido cancelado. A mesa foi liberada e o estoque vinculado foi devolvido."); reload(); }
            }, false));
            page.Controls.Add(actions); page.Controls.Add(grid); reload();
        }

        private void ShowPartial(int id)
        {
            Order order = state.Orders.FirstOrDefault(x => x.Id == id); if (order == null) { Alert("Selecione uma comanda."); return; }
            using (PartialAccountForm form = new PartialAccountForm(order, delegate { Print(order, false, true); })) form.ShowDialog(this);
        }

        private void NewOrder()
        {
            using (NewOrderForm form = new NewOrderForm(state.Products, state.NextOrderId))
            {
                if (form.ShowDialog(this) == DialogResult.OK) {
                    PrepareNewOrder(form.CreatedOrder);
                    state.Orders.Add(form.CreatedOrder); state.NextOrderId++; Save();
                    if (state.AutoPrintOrder) Print(form.CreatedOrder, false);
                    else if (MessageBox.Show("Comanda criada. Imprimir pedido agora?", "Ambrosia", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) Print(form.CreatedOrder, false);
                }
            }
        }

        private void NewCounterOrder()
        {
            string label = "Balcão " + state.NextOrderId.ToString("000");
            using (NewOrderForm form = new NewOrderForm(state.Products, state.NextOrderId, label))
            {
                if (form.ShowDialog(this) == DialogResult.OK)
                {
                    PrepareNewOrder(form.CreatedOrder);
                    state.Orders.Add(form.CreatedOrder); state.NextOrderId++; Save();
                    if (state.AutoPrintOrder) Print(form.CreatedOrder, false);
                }
            }
        }

        private void ReceiveOrder(int id)
        {
            Order order = state.Orders.FirstOrDefault(x => x.Id == id); if (order == null) { Alert("Selecione uma comanda."); return; }
            bool isTable = TableNumberFromLabel(order.Label) > 0;
            if (order.Status == "Pago") { Alert(isTable ? "Esta mesa já foi liberada." : "Esta comanda já foi paga."); return; }
            if (order.Status == "Cancelado") { Alert(isTable ? "Esta mesa foi cancelada." : "Esta comanda foi cancelada."); return; }
            if (state.ConfirmBeforePayment && order.PaidAmount <= 0 && MessageBox.Show("Abrir o recebimento de " + order.Total.ToString("C2") + " para " + order.Label + "?", "Ambrosia", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
            string[] methods = new string[] { "Pix", "Dinheiro", "Crédito", "Débito" }.OrderBy(x => x == state.DefaultPaymentMethod ? 0 : 1).ToArray();
            bool wasPaid = order.Status == "Pago";
            using (PaymentForm payment = new PaymentForm(order, methods, delegate(string method, decimal amount)
            {
                if (amount <= 0 || amount > order.Remaining + 0.005m) return false;
                string stamp = DateTime.Now.ToString("o");
                order.Payments.Add(new PaymentEntry { LocalId = Guid.NewGuid().ToString("N"), Method = method, Amount = amount, CreatedAt = stamp });
                state.Transactions.Add(new Transaction { Description = "Recebimento " + order.Label, Amount = amount, Method = method, CreatedAt = stamp });
                if (order.Remaining <= 0.005m)
                {
                    foreach (OrderLine line in order.Lines.Where(x => StateStore.LineIsFood(x) && String.IsNullOrWhiteSpace(x.ServedAt))) line.ServedAt = stamp;
                    order.Status = "Pago"; order.ClosedAt = stamp;
                    order.PaymentMethod = order.Payments.Select(x => x.Method).Distinct().Count() > 1 ? "Múltiplos" : method;
                    StateStore.RefreshKitchenStartedAt(order);
                }
                Save(); return true;
            }))
            {
                payment.ShowDialog(this);
            }
            if (!wasPaid && order.Status == "Pago") TryUploadPaidOrders(false);
            if (!wasPaid && order.Status == "Pago" && state.AskPrintReceipt && MessageBox.Show(isTable ? "Conta recebida e mesa liberada. Imprimir comprovante?" : "Conta recebida. Imprimir comprovante?", "Ambrosia", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) Print(order, true, false);
        }

        private void PrintOrder(int id)
        {
            Order order = state.Orders.FirstOrDefault(x => x.Id == id); if (order == null) { Alert("Selecione uma comanda."); return; }
            Print(order, order.Status == "Pago", order.Status != "Pago");
        }

        private void Print(Order order, bool receipt) { Print(order, receipt, false); }

        private float ReceiptFontPointSize(bool bold)
        {
            string selected = String.IsNullOrWhiteSpace(state.PrintFontSize) ? "Média" : state.PrintFontSize;
            if (selected == "Muito pequena") return bold ? (state.PaperWidth == 58 ? 5.5f : 6.5f) : (state.PaperWidth == 58 ? 5f : 6f);
            if (selected == "Pequena") return bold ? (state.PaperWidth == 58 ? 6.5f : 7.5f) : (state.PaperWidth == 58 ? 6f : 7f);
            if (selected == "Grande") return bold ? (state.PaperWidth == 58 ? 9.5f : 11.5f) : (state.PaperWidth == 58 ? 8.5f : 10.5f);
            if (selected == "Muito grande") return bold ? (state.PaperWidth == 58 ? 11f : 13f) : (state.PaperWidth == 58 ? 10f : 12f);
            return bold ? (state.PaperWidth == 58 ? 7.5f : 9f) : (state.PaperWidth == 58 ? 7f : 8.5f);
        }

        private string PrintFontFamily()
        {
            string selected = String.IsNullOrWhiteSpace(state.PrintFontName) ? "Consolas" : state.PrintFontName;
            try { using (Font test = new Font(selected, 8f)) return test.FontFamily.Name; }
            catch { return "Consolas"; }
        }

        private void Print(Order order, bool receipt, bool partial)
        {
            if (String.IsNullOrWhiteSpace(state.PrinterName)) { Alert("Escolha a Elgin i8 em Configurações, na guia Impressão."); return; }
            try
            {
                PrintDocument doc = new PrintDocument(); doc.PrinterSettings.PrinterName = state.PrinterName; doc.PrintController = new StandardPrintController();
                bool containsFood = !receipt && !partial && order.Lines != null && order.Lines.Any(x => StateStore.LineIsFood(x));
                doc.PrinterSettings.Copies = Convert.ToInt16(containsFood ? 2 : Math.Max(1, Math.Min(3, state.PrintCopies)));
                int paperWidth = state.PaperWidth == 58 ? 228 : 315;
                doc.DefaultPageSettings.PaperSize = new PaperSize(state.PaperWidth + " mm", paperWidth, 1100); doc.DefaultPageSettings.Margins = new Margins(8, 8, 5, 5);
                doc.PrintPage += delegate(object sender, PrintPageEventArgs e) {
                    float normalSize = ReceiptFontPointSize(false); float boldSize = ReceiptFontPointSize(true);
                    Font normal = new Font(PrintFontFamily(), normalSize); Font bold = new Font(PrintFontFamily(), boldSize, FontStyle.Bold); float y = 8;
                    Action<string, Font> line = delegate(string text, Font font) { e.Graphics.DrawString(text, font, Brushes.Black, 8, y); y += font.GetHeight(e.Graphics) + 3; };
                    string documentTitle = receipt ? "COMPROVANTE DE PAGAMENTO" : (partial ? "PARCIAL DA CONTA" : (order.Status == "Novo lançamento" ? "NOVO LANCAMENTO - MESA" : "PEDIDO / COMANDA"));
                    line(state.RestaurantName.ToUpperInvariant(), bold); line(state.RestaurantSubtitle, normal); line(documentTitle, bold); line("--------------------------------", normal);
                    line(order.Label + "  #" + order.Id, bold); line("Cliente: " + (String.IsNullOrWhiteSpace(order.Customer) ? "Não informado" : order.Customer), normal); line("--------------------------------", normal);
                    foreach (OrderLine item in order.Lines) line(item.Quantity.ToString("0.##") + "x " + item.Name + "  " + item.Total.ToString("C2"), normal);
                    if ((partial || receipt) && order.Cancellations != null && order.Cancellations.Count > 0)
                    {
                        line("--------------------------------", normal); line("CANCELAMENTOS REGISTRADOS", bold);
                        foreach (CancellationEntry cancellation in order.Cancellations.Where(x => x.Scope == "Item").Take(8))
                            line(cancellation.Quantity.ToString("0.##") + "x " + cancellation.ItemName + " - " + cancellation.Reason, normal);
                    }
                    line("--------------------------------", normal);
                    if (partial || receipt)
                    {
                        line("Subtotal consumo: " + order.Subtotal.ToString("C2"), normal);
                        line("Taxa atendimento (10%): " + order.ServiceFee.ToString("C2"), normal);
                    }
                    line("TOTAL: " + order.Total.ToString("C2"), bold);
                    if (order.PaidAmount > 0) line("Valor recebido: " + order.PaidAmount.ToString("C2"), normal);
                    if (!receipt && order.PaidAmount > 0) line("Falta receber: " + order.Remaining.ToString("C2"), bold);
                    if (receipt)
                    {
                        line("PAGAMENTOS", bold);
                        if (order.Payments.Count > 0) foreach (PaymentEntry payment in order.Payments) line(payment.Method + ": " + payment.Amount.ToString("C2"), normal);
                        else line("Pagamento: " + order.PaymentMethod, normal);
                    }
                    line(DateTime.Now.ToString("dd/MM/yyyy HH:mm"), normal); line(state.ReceiptFooter, normal); e.HasMorePages = false;
                    normal.Dispose(); bold.Dispose();
                };
                doc.Print(); doc.Dispose();
            }
            catch (Exception ex) { Alert("Não foi possível imprimir na " + state.PrinterName + ".\r\n" + ex.Message); }
        }

        private void PrintCancellation(Order order, CancellationEntry cancellation)
        {
            if (String.IsNullOrWhiteSpace(state.PrinterName))
            {
                Alert("O cancelamento foi salvo, mas não foi impresso. Escolha a Elgin i8 em Configurações, na guia Impressão.");
                return;
            }
            try
            {
                PrintDocument doc = new PrintDocument(); doc.PrinterSettings.PrinterName = state.PrinterName; doc.PrintController = new StandardPrintController();
                doc.PrinterSettings.Copies = Convert.ToInt16(Math.Max(1, Math.Min(3, state.PrintCopies)));
                int paperWidth = state.PaperWidth == 58 ? 228 : 315;
                doc.DefaultPageSettings.PaperSize = new PaperSize(state.PaperWidth + " mm", paperWidth, 700); doc.DefaultPageSettings.Margins = new Margins(8, 8, 5, 5);
                doc.PrintPage += delegate(object sender, PrintPageEventArgs e) {
                    float normalSize = ReceiptFontPointSize(false); float boldSize = ReceiptFontPointSize(true);
                    Font normal = new Font(PrintFontFamily(), normalSize); Font bold = new Font(PrintFontFamily(), boldSize, FontStyle.Bold); float y = 8;
                    Action<string, Font> line = delegate(string text, Font font) { e.Graphics.DrawString(text, font, Brushes.Black, 8, y); y += font.GetHeight(e.Graphics) + 3; };
                    line(state.RestaurantName.ToUpperInvariant(), bold); line(state.RestaurantSubtitle, normal);
                    line(cancellation.Scope == "Item" ? "CANCELAMENTO DE ITEM" : "CANCELAMENTO DE PEDIDO", bold); line("--------------------------------", normal);
                    line(order.Label + "  #" + order.Id, bold);
                    if (cancellation.Scope == "Item")
                    {
                        line("Item: " + cancellation.ItemName, normal);
                        line("Quantidade: " + cancellation.Quantity.ToString("0.##"), normal);
                    }
                    else line("ATENDIMENTO CANCELADO", bold);
                    foreach (Match part in Regex.Matches("Motivo: " + cancellation.Reason, @".{1,32}(?:\s+|$)|\S+")) line(part.Value.Trim(), normal);
                    DateTime stamp; string printedStamp = DateTime.TryParse(cancellation.CreatedAt, null, DateTimeStyles.RoundtripKind, out stamp) ? stamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss") : DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss");
                    line("Data: " + printedStamp, normal); line("--------------------------------", normal);
                    if (cancellation.Scope == "Item") line("Novo total: " + order.Total.ToString("C2"), bold);
                    line("VIA DE CANCELAMENTO", bold); e.HasMorePages = false;
                    normal.Dispose(); bold.Dispose();
                };
                doc.Print(); doc.Dispose();
            }
            catch (Exception ex)
            {
                Alert("O cancelamento foi salvo, mas não foi possível imprimir na " + state.PrinterName + ".\r\n" + ex.Message);
            }
        }

        private void RegisterCashWithdrawal(bool returnToCash)
        {
            string description = PromptForm.Ask("Motivo da sangria", "Ex.: compra de gelo");
            if (String.IsNullOrWhiteSpace(description)) return;
            decimal amount;
            if (!Decimal.TryParse(PromptForm.Ask("Valor da sangria", "0,00"), out amount) || amount <= 0) { Alert("Valor inválido."); return; }
            if (MessageBox.Show("Registrar sangria de " + amount.ToString("C2") + "?", "Confirmar sangria", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
            state.Transactions.Add(new Transaction { Description = "Sangria · " + description.Trim(), Amount = -amount, Method = "Dinheiro", CreatedAt = DateTime.Now.ToString("o") });
            Save(); Alert("Sangria registrada com sucesso."); if (returnToCash) ShowCash(); else ShowDashboard();
        }

        private void ShowCash()
        {
            FlowLayoutPanel page = StartPage("Caixa", "Recebimentos separados por forma de pagamento e fechamento diário.");
            List<Transaction> today = state.Transactions.Where(x => IsToday(x.CreatedAt)).ToList();
            decimal incoming = today.Where(x => x.Amount > 0).Sum(x => x.Amount); decimal outgoing = Math.Abs(today.Where(x => x.Amount < 0).Sum(x => x.Amount));
            decimal pix = today.Where(x => x.Amount > 0 && String.Equals(x.Method, "Pix", StringComparison.OrdinalIgnoreCase)).Sum(x => x.Amount);
            decimal cash = today.Where(x => x.Amount > 0 && String.Equals(x.Method, "Dinheiro", StringComparison.OrdinalIgnoreCase)).Sum(x => x.Amount);
            decimal credit = today.Where(x => x.Amount > 0 && String.Equals(x.Method, "Crédito", StringComparison.OrdinalIgnoreCase)).Sum(x => x.Amount);
            decimal debit = today.Where(x => x.Amount > 0 && String.Equals(x.Method, "Débito", StringComparison.OrdinalIgnoreCase)).Sum(x => x.Amount);
            decimal other = incoming - pix - cash - credit - debit;
            page.Controls.Add(Theme.Label("RECEBIMENTOS POR FORMA DE PAGAMENTO", 9, FontStyle.Bold, Theme.Orange));
            FlowLayoutPanel paymentStats = new FlowLayoutPanel { Width = 950, Height = 132, WrapContents = false };
            paymentStats.Controls.Add(Stat("PIX", pix.ToString("C2"), "Recebido hoje"));
            paymentStats.Controls.Add(Stat("DINHEIRO", cash.ToString("C2"), "Recebido hoje"));
            paymentStats.Controls.Add(Stat("CRÉDITO", credit.ToString("C2"), "Recebido hoje"));
            paymentStats.Controls.Add(Stat("DÉBITO", debit.ToString("C2"), "Recebido hoje"));
            page.Controls.Add(paymentStats);
            FlowLayoutPanel stats = new FlowLayoutPanel { Width = 950, Height = 132, WrapContents = false };
            stats.Controls.Add(Stat("TOTAL RECEBIDO", incoming.ToString("C2"), other > 0 ? "Outros: " + other.ToString("C2") : "Pix + dinheiro + cartões"));
            stats.Controls.Add(Stat("SAÍDAS", outgoing.ToString("C2"), "Despesas do dia"));
            stats.Controls.Add(Stat("SALDO LÍQUIDO", (incoming - outgoing).ToString("C2"), "Recebimentos menos saídas"));
            page.Controls.Add(stats);
            CashClosure existingClosure = state.CashClosures.FirstOrDefault(x => IsToday(x.ClosedAt));
            if (existingClosure != null)
            {
                DateTime closedStamp; string closedTime = DateTime.TryParse(existingClosure.ClosedAt, null, DateTimeStyles.RoundtripKind, out closedStamp) ? closedStamp.ToLocalTime().ToString("HH:mm") : "hoje";
                page.Controls.Add(Theme.Label("✓ Fechamento diário salvo às " + closedTime + " · Saldo líquido " + existingClosure.NetTotal.ToString("C2"), 9, FontStyle.Bold, Color.FromArgb(45, 115, 87)));
            }
            FlowLayoutPanel actions = new FlowLayoutPanel { Width = 950, Height = 48, WrapContents = false };
            Button expense = Theme.Button("- Registrar sangria", delegate { RegisterCashWithdrawal(true); }, false); expense.Width = 175; actions.Controls.Add(expense);
            Button closeCash = Theme.Button(existingClosure == null ? "Fechar caixa e sair" : "Atualizar caixa e sair", delegate {
                string summary = "Pix: " + pix.ToString("C2") + "\r\nDinheiro: " + cash.ToString("C2") + "\r\nCrédito: " + credit.ToString("C2") + "\r\nDébito: " + debit.ToString("C2") + (other > 0 ? "\r\nOutros: " + other.ToString("C2") : "") + "\r\n\r\nTotal recebido: " + incoming.ToString("C2") + "\r\nSaídas: " + outgoing.ToString("C2") + "\r\nSaldo líquido: " + (incoming - outgoing).ToString("C2");
                if (MessageBox.Show(summary + "\r\n\r\nConfirmar fechamento diário do caixa?", "Fechamento diário", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
                CashClosure closure = state.CashClosures.FirstOrDefault(x => IsToday(x.ClosedAt));
                if (closure == null) { closure = new CashClosure(); state.CashClosures.Add(closure); }
                closure.ClosedAt = DateTime.Now.ToString("o"); closure.Pix = pix; closure.Cash = cash; closure.Credit = credit; closure.Debit = debit; closure.Other = other; closure.Expenses = outgoing;
                Save(); MessageBox.Show("Caixa fechado e salvo com sucesso.\r\n\r\nO programa será encerrado. Para abrir o caixa novamente, entre com seu usuário na tela de login.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Information); Close();
            }, true); closeCash.Width = 210; actions.Controls.Add(closeCash); page.Controls.Add(actions);
            DataGridView grid = Grid(); grid.Height = 250; grid.Columns.Add("Time", "Horário"); grid.Columns.Add("Description", "Descrição"); grid.Columns.Add("Method", "Forma"); grid.Columns.Add("Amount", "Valor");
            foreach (Transaction item in today.OrderByDescending(x => x.CreatedAt)) grid.Rows.Add(DateTime.Parse(item.CreatedAt).ToLocalTime().ToString("HH:mm"), item.Description, item.Method, item.Amount.ToString("C2"));
            page.Controls.Add(grid);
        }

        private void ShowInventory()
        {
            if (currentUserRole != "admin") { Alert("Acesso permitido somente ao Administrador."); ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Estoque", "Controle local de quantidades, mínimos e custo médio.");
            DataGridView grid = Grid(); grid.Columns.Add("Item", "Item"); grid.Columns.Add("Quantity", "Quantidade"); grid.Columns.Add("Minimum", "Mínimo"); grid.Columns.Add("Cost", "Custo unitário"); grid.Columns.Add("State", "Situação");
            Action reload = delegate { grid.Rows.Clear(); foreach (InventoryItem item in state.Inventory) { int row = grid.Rows.Add(item.Name, item.Quantity.ToString("0.##") + " " + item.Unit, item.Minimum.ToString("0.##") + " " + item.Unit, item.UnitCost.ToString("C2"), item.Quantity <= item.Minimum ? "REPOR" : "OK"); grid.Rows[row].Tag = item.Name; } };
            Button adjust = Theme.Button("Ajustar quantidade", delegate {
                if (grid.SelectedRows.Count == 0) { Alert("Selecione um item."); return; } string name = Convert.ToString(grid.SelectedRows[0].Tag); InventoryItem item = state.Inventory.First(x => x.Name == name);
                decimal value; if (!Decimal.TryParse(PromptForm.Ask("Nova quantidade de " + item.Name, item.Quantity.ToString("0.##")), out value) || value < 0) { Alert("Quantidade inválida."); return; }
                item.Quantity = value; Save(); reload();
            }, true); page.Controls.Add(adjust); page.Controls.Add(grid); reload();
        }

        private void ShowProducts()
        {
            ShowMenu(); pageTitle.Text = "Produtos e categorias";
        }

        private void ShowMenu()
        {
            if (currentUserRole != "admin") { Alert("Acesso permitido somente ao Administrador."); ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Cardápio e preços", "Consulte os produtos, preços e categorias recebidos do ERP.");
            Label locked = Theme.Label("CADASTRO BLOQUEADO NESTE COMPUTADOR · Faça alterações no ERP online e clique em Sincronizar.", 9, FontStyle.Bold, Theme.Orange);
            locked.MaximumSize = new Size(900, 0); page.Controls.Add(locked);
            FlowLayoutPanel filters = new FlowLayoutPanel { Width = 950, Height = 52, WrapContents = false };
            TextBox search = new TextBox { Width = 430, Font = new Font("Arial", 10, FontStyle.Bold), Margin = new Padding(3, 9, 12, 3) };
            search.Text = "";
            ComboBox categoryFilter = new ComboBox { Width = 240, DropDownStyle = ComboBoxStyle.DropDownList, Margin = new Padding(3, 8, 12, 3), Font = new Font("Arial", 9, FontStyle.Bold) };
            Label searchHint = Theme.Label("Pesquisar produto:", 9, FontStyle.Bold, Theme.Brown); searchHint.Margin = new Padding(3, 13, 8, 3);
            filters.Controls.Add(searchHint); filters.Controls.Add(search); filters.Controls.Add(categoryFilter);

            DataGridView grid = Grid(); grid.Height = 350; grid.Columns.Add("Name", "Produto"); grid.Columns.Add("Category", "Categoria"); grid.Columns.Add("Price", "Preço"); grid.Columns.Add("Cost", "Custo"); grid.Columns.Add("State", "Status");
            Action refreshCategories = delegate {
                string selected = Convert.ToString(categoryFilter.SelectedItem);
                categoryFilter.Items.Clear(); categoryFilter.Items.Add("Todas as categorias");
                foreach (string category in state.ProductCategories.OrderBy(x => x)) categoryFilter.Items.Add(category);
                categoryFilter.SelectedItem = categoryFilter.Items.Contains(selected) ? selected : "Todas as categorias";
            };
            Action reload = delegate {
                grid.Rows.Clear(); string query = search.Text.Trim(); string category = Convert.ToString(categoryFilter.SelectedItem);
                IEnumerable<Product> source = state.Products;
                if (!String.IsNullOrWhiteSpace(query)) source = source.Where(x => x.Name.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0);
                if (!String.IsNullOrWhiteSpace(category) && category != "Todas as categorias") source = source.Where(x => String.Equals(x.Category, category, StringComparison.OrdinalIgnoreCase));
                foreach (Product product in source.OrderBy(x => x.Category).ThenBy(x => x.Name)) { int row = grid.Rows.Add(product.Name, product.Category, product.Price.ToString("C2"), product.Cost.ToString("C2"), product.Active ? "Ativo" : "Pausado"); grid.Rows[row].Tag = product; }
            };
            search.TextChanged += delegate { reload(); }; categoryFilter.SelectedIndexChanged += delegate { reload(); };

            FlowLayoutPanel actions = new FlowLayoutPanel { Width = 950, Height = 48, WrapContents = false };
            Button sync = Theme.Button("↻ Sincronizar agora", delegate { SyncFromErp(); }, true); sync.Width = 190; actions.Controls.Add(sync);
            page.Controls.Add(filters); page.Controls.Add(actions); page.Controls.Add(grid); refreshCategories(); reload();
        }

        private void ShowTeam()
        {
            if (currentUserRole != "admin") { Alert("Acesso permitido somente ao Administrador."); ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Funcionários e ponto", "Registre entrada e saída no computador do restaurante.");
            DataGridView grid = Grid(); grid.Columns.Add("Name", "Funcionário"); grid.Columns.Add("Role", "Função"); grid.Columns.Add("State", "Situação"); grid.Columns.Add("Time", "Horário");
            Action reload = delegate { grid.Rows.Clear(); foreach (Employee employee in state.Employees) { int row = grid.Rows.Add(employee.Name, employee.Role, employee.Working ? "Trabalhando" : "Fora do turno", employee.Working ? DateTime.Parse(employee.ClockIn).ToLocalTime().ToString("HH:mm") : "—"); grid.Rows[row].Tag = employee.Name; } };
            FlowLayoutPanel actions = new FlowLayoutPanel { Width = 950, Height = 48 };
            actions.Controls.Add(Theme.Button("Registrar entrada / saída", delegate { if (grid.SelectedRows.Count == 0) { Alert("Selecione um funcionário."); return; } Employee employee = state.Employees.First(x => x.Name == Convert.ToString(grid.SelectedRows[0].Tag)); if (employee.Working) { employee.Working = false; employee.LastClockOut = DateTime.Now.ToString("o"); } else { employee.Working = true; employee.ClockIn = DateTime.Now.ToString("o"); } Save(); reload(); }, true));
            actions.Controls.Add(Theme.Button("+ Novo funcionário", delegate { string name = PromptForm.Ask("Nome do funcionário", "Nome completo"); if (String.IsNullOrWhiteSpace(name)) return; string role = PromptForm.Ask("Função", "Atendimento"); state.Employees.Add(new Employee { Name = name, Role = role }); Save(); reload(); }, false));
            page.Controls.Add(actions); page.Controls.Add(grid); reload();
        }

        private void ShowUsersPermissions()
        {
            if (currentUserRole != "admin") { Alert("Acesso permitido somente ao Administrador."); ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Usuários e permissões", "Cadastre usuários e escolha o acesso conforme a função no estabelecimento.");
            page.Controls.Add(Theme.Label("ADMINISTRADOR: tudo   •   ATENDIMENTO: transferir mesa, imprimir parcial, lançar e cancelar produtos   •   CAIXA: somente caixa", 9, FontStyle.Bold, Theme.Orange));
            DataGridView grid = Grid(); grid.Height = 340; grid.Columns.Add("Name", "Nome"); grid.Columns.Add("Username", "Usuário"); grid.Columns.Add("Role", "Função"); grid.Columns.Add("Transfer", "Transferir mesa"); grid.Columns.Add("Partial", "Imprimir parcial"); grid.Columns.Add("Add", "Lançar produto"); grid.Columns.Add("Cancel", "Cancelar produto"); grid.Columns.Add("Password", "Trocar senha"); grid.Columns.Add("State", "Status");
            Action reload = delegate {
                grid.Rows.Clear();
                foreach (AppUser user in state.AppUsers.OrderBy(x => x.DisplayName))
                {
                    string role = String.Equals(user.Role, "admin", StringComparison.OrdinalIgnoreCase) ? "ADMINISTRADOR" : (String.Equals(user.Role, "caixa", StringComparison.OrdinalIgnoreCase) ? "CAIXA" : "ATENDIMENTO");
                    bool admin = role == "ADMINISTRADOR"; bool attendance = role == "ATENDIMENTO";
                    int row = grid.Rows.Add(user.DisplayName, user.Username, role, admin || attendance ? "SIM" : "—", admin || attendance ? "SIM" : "—", admin || attendance ? "SIM" : "—", admin || attendance ? "SIM" : "—", admin ? "SIM" : "—", user.Active ? "ATIVO" : "BLOQUEADO"); grid.Rows[row].Tag = user;
                }
            };
            FlowLayoutPanel actions = new FlowLayoutPanel { Width = 950, Height = 52, WrapContents = false };
            Button add = Theme.Button("+ Cadastrar usuário", delegate {
                using (UserEditorForm form = new UserEditorForm(null))
                {
                    if (form.ShowDialog(this) != DialogResult.OK) return;
                    if (state.AppUsers.Any(x => String.Equals(x.Username, form.UserName, StringComparison.OrdinalIgnoreCase))) { Alert("Já existe um usuário com esse nome de acesso."); return; }
                    state.AppUsers.Add(new AppUser { DisplayName = form.DisplayName, Username = form.UserName, PasswordHash = StateStore.HashPassword(form.Password), Role = form.UserRole, Active = true }); Save(); reload();
                }
            }, true); add.Width = 175; actions.Controls.Add(add);
            Button edit = Theme.Button("Editar usuário", delegate {
                if (grid.SelectedRows.Count == 0 || grid.SelectedRows[0].Tag == null) { Alert("Selecione um usuário."); return; }
                AppUser user = (AppUser)grid.SelectedRows[0].Tag; bool master = String.Equals(user.Username, "ambrosia adm", StringComparison.OrdinalIgnoreCase);
                using (UserEditorForm form = new UserEditorForm(user))
                {
                    if (form.ShowDialog(this) != DialogResult.OK) return;
                    if (state.AppUsers.Any(x => !Object.ReferenceEquals(x, user) && String.Equals(x.Username, form.UserName, StringComparison.OrdinalIgnoreCase))) { Alert("Já existe outro usuário com esse nome de acesso."); return; }
                    user.DisplayName = form.DisplayName; user.Username = master ? "ambrosia adm" : form.UserName; user.Role = master ? "admin" : form.UserRole;
                    if (!String.IsNullOrWhiteSpace(form.Password)) user.PasswordHash = StateStore.HashPassword(form.Password);
                    Save(); reload();
                }
            }, false); edit.Width = 145; actions.Controls.Add(edit);
            Button toggle = Theme.Button("Ativar / bloquear", delegate {
                if (grid.SelectedRows.Count == 0 || grid.SelectedRows[0].Tag == null) { Alert("Selecione um usuário."); return; }
                AppUser user = (AppUser)grid.SelectedRows[0].Tag;
                if (String.Equals(user.Username, "ambrosia adm", StringComparison.OrdinalIgnoreCase)) { Alert("O administrador mestre não pode ser bloqueado."); return; }
                user.Active = !user.Active; Save(); reload();
            }, false); toggle.Width = 155; actions.Controls.Add(toggle);
            page.Controls.Add(actions); page.Controls.Add(grid); reload();
        }

        private void ShowReports()
        {
            if (currentUserRole != "admin") { Alert("Acesso permitido somente ao Administrador."); ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Relatórios", "Resumo calculado com os dados locais do aplicativo.");
            List<Order> paid = state.Orders.Where(x => x.Status == "Pago" && IsToday(x.ClosedAt)).ToList(); decimal sales = paid.Sum(x => x.Total); decimal cost = paid.Sum(x => x.Cost);
            FlowLayoutPanel stats = new FlowLayoutPanel { Width = 950, Height = 140 };
            stats.Controls.Add(Stat("FATURAMENTO", sales.ToString("C2"), "Hoje")); stats.Controls.Add(Stat("PEDIDOS PAGOS", paid.Count.ToString(), "Hoje")); stats.Controls.Add(Stat("LUCRO BRUTO", (sales - cost).ToString("C2"), "Antes das despesas")); stats.Controls.Add(Stat("CMV", sales == 0 ? "0,0%" : (cost / sales).ToString("P1"), "Custo sobre vendas")); page.Controls.Add(stats);
            page.Controls.Add(Theme.Label("PRODUTOS MAIS VENDIDOS", 10, FontStyle.Bold, Theme.Orange));
            DataGridView grid = Grid(); grid.Height = 280; grid.Columns.Add("Product", "Produto"); grid.Columns.Add("Quantity", "Quantidade"); grid.Columns.Add("Revenue", "Faturamento");
            var ranking = paid.SelectMany(x => x.Lines).GroupBy(x => x.Name).Select(g => new { Name = g.Key, Quantity = g.Sum(x => x.Quantity), Revenue = g.Sum(x => x.Total) }).OrderByDescending(x => x.Revenue).Take(12);
            foreach (var item in ranking) grid.Rows.Add(item.Name, item.Quantity.ToString("0.##"), item.Revenue.ToString("C2")); page.Controls.Add(grid);
        }

        private void ShowSync()
        {
            if (currentUserRole != "admin") { ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Sincronização ERP", "Receba automaticamente as alterações feitas no ERP online.");
            Panel card = new Panel { Width = 920, Height = 390, BackColor = Color.White, Padding = new Padding(28), Margin = new Padding(0, 14, 0, 10) };
            Label title = Theme.Label("SINCRONIZAÇÃO ONLINE", 11, FontStyle.Bold, Theme.Orange); title.Location = new Point(28, 28); card.Controls.Add(title);
            string statusText = state.LastSyncRevision > 0
                ? "Última sincronização: revisão " + state.LastSyncRevision + " · " + SyncDateText(state.LastSyncAt)
                : "Este computador ainda não recebeu um pacote do ERP.";
            Label status = Theme.Label(statusText, 13, FontStyle.Bold, Theme.Brown); status.Location = new Point(28, 67); status.MaximumSize = new Size(830, 0); card.Controls.Add(status);
            Label updates = Theme.Label("Atualiza categorias, produtos e preços diretamente do ERP. Após o primeiro acesso, basta um clique.", 9, FontStyle.Regular, Theme.Muted); updates.Location = new Point(28, 112); updates.MaximumSize = new Size(830, 0); card.Controls.Add(updates);
            Label preserves = Theme.Label("Mesas abertas, pedidos, pagamentos, cancelamentos e fechamentos locais não são apagados.", 9, FontStyle.Bold, Color.FromArgb(45, 115, 87)); preserves.Location = new Point(28, 148); preserves.MaximumSize = new Size(830, 0); card.Controls.Add(preserves);
            Button online = Theme.Button("↻ Sincronizar agora", delegate { SyncFromErp(); }, true); online.Location = new Point(28, 199); online.Width = 220; card.Controls.Add(online);
            Button changeAccount = Theme.Button("Trocar acesso do ERP", delegate { state.ErpRefreshTokenProtected = ""; Save(); SyncFromErp(); }, false); changeAccount.Location = new Point(266, 199); changeAccount.Width = 185; card.Controls.Add(changeAccount);
            Button import = Theme.Button("Importar arquivo antigo", delegate { ImportSyncPackage(); }, false); import.Location = new Point(469, 199); import.Width = 185; card.Controls.Add(import);
            Label downloadHint = Theme.Label("A senha é usada somente para entrar. O acesso contínuo fica protegido pelo Windows para sincronizações futuras.", 8, FontStyle.Bold, Theme.Orange); downloadHint.Location = new Point(28, 251); downloadHint.MaximumSize = new Size(830, 0); card.Controls.Add(downloadHint);
            Button openData = Theme.Button("Abrir pasta de dados", delegate { Process.Start("explorer.exe", StateStore.DataFolder); }, false); openData.Location = new Point(28, 292); openData.Width = 185; card.Controls.Add(openData);
            Label nextLogin = Theme.Label("Os novos usuários e permissões passam a valer no próximo acesso ao aplicativo.", 8, FontStyle.Bold, Theme.Muted); nextLogin.Location = new Point(232, 303); nextLogin.MaximumSize = new Size(620, 0); card.Controls.Add(nextLogin);
            page.Controls.Add(card);
            page.Controls.Add(Theme.Label("Categorias e produtos só podem ser alterados no ERP online. O aplicativo Windows mantém mesas, pedidos e pagamentos locais.", 9, FontStyle.Bold, Theme.Muted));
        }

        private void SyncFromErp()
        {
            Cursor previousCursor = Cursor.Current;
            try
            {
                Cursor.Current = Cursors.WaitCursor;
                ErpAuthSession session = null;
                string refreshToken = StateStore.UnprotectSecret(state.ErpRefreshTokenProtected);
                if (!String.IsNullOrWhiteSpace(refreshToken))
                {
                    try { session = ErpSyncClient.Refresh(refreshToken); }
                    catch { state.ErpRefreshTokenProtected = ""; Save(); }
                }

                if (session == null || String.IsNullOrWhiteSpace(session.access_token))
                {
                    Cursor.Current = previousCursor;
                    using (ErpCredentialsForm credentials = new ErpCredentialsForm(state.ErpEmail))
                    {
                        if (credentials.ShowDialog(this) != DialogResult.OK) return;
                        Cursor.Current = Cursors.WaitCursor;
                        session = ErpSyncClient.SignIn(credentials.Email, credentials.Password);
                        state.ErpEmail = credentials.Email.Trim();
                    }
                }

                if (session == null || String.IsNullOrWhiteSpace(session.access_token))
                    throw new InvalidOperationException("O ERP não autorizou este acesso. Confira o e-mail e a senha cadastrados na área do cliente.");

                if (!String.IsNullOrWhiteSpace(session.refresh_token))
                    state.ErpRefreshTokenProtected = StateStore.ProtectSecret(session.refresh_token);

                ErpAuthUser user = session.user ?? ErpSyncClient.Get<ErpAuthUser>("/auth/v1/user", session.access_token);
                if (user == null || String.IsNullOrWhiteSpace(user.id)) throw new InvalidOperationException("Não foi possível identificar o usuário do ERP.");

                List<ErpMember> memberships = ErpSyncClient.Get<List<ErpMember>>("/rest/v1/company_members?select=company_id&user_id=eq." + Uri.EscapeDataString(user.id) + "&active=eq.true&limit=1", session.access_token);
                if (memberships == null || memberships.Count == 0) throw new InvalidOperationException("Este usuário ainda não está vinculado a uma empresa no ERP.");
                long companyId = memberships[0].company_id;

                List<ErpCategory> categories = ErpSyncClient.Get<List<ErpCategory>>("/rest/v1/categories?select=id,name,sort_order,active,revision&company_id=eq." + companyId + "&order=sort_order.asc,name.asc", session.access_token) ?? new List<ErpCategory>();
                List<ErpProduct> products = ErpSyncClient.Get<List<ErpProduct>>("/rest/v1/products?select=id,category_id,sku,name,description,price,cost,unit,track_stock,active,revision&company_id=eq." + companyId + "&order=name.asc", session.access_token) ?? new List<ErpProduct>();
                List<ErpRevision> revisions = ErpSyncClient.Get<List<ErpRevision>>("/rest/v1/sync_revisions?select=id&company_id=eq." + companyId + "&order=id.desc&limit=1", session.access_token) ?? new List<ErpRevision>();

                int[] uploaded = UploadOperationalData(companyId, user.id, session.access_token, products, revisions);

                Dictionary<long, string> categoryNames = categories.ToDictionary(x => x.id, x => x.name ?? "Sem categoria");
                state.ProductCategories = categories.Where(x => !String.IsNullOrWhiteSpace(x.name)).OrderBy(x => x.sort_order).ThenBy(x => x.name).Select(x => x.name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
                state.Products = products.Select(x => new Product {
                    Name = String.IsNullOrWhiteSpace(x.name) ? "Produto sem nome" : x.name.Trim(),
                    Category = x.category_id.HasValue && categoryNames.ContainsKey(x.category_id.Value) ? categoryNames[x.category_id.Value] : "Sem categoria",
                    Price = x.price, Cost = x.cost, Active = x.active
                }).ToList();
                if (!state.ProductCategories.Contains("Sem categoria") && state.Products.Any(x => x.Category == "Sem categoria")) state.ProductCategories.Add("Sem categoria");

                state.LastSyncAt = DateTime.Now.ToString("o");
                state.LastSyncRevision = revisions.Count == 0 ? state.LastSyncRevision : (int)Math.Min(Int32.MaxValue, revisions[0].id);
                Save();
                Cursor.Current = previousCursor;
                Alert("Sincronização concluída.\r\n\r\n" + state.ProductCategories.Count + " categorias e " + state.Products.Count + " produtos atualizados.\r\n" + uploaded[0] + " pedidos, " + uploaded[1] + " movimentações de caixa, " + uploaded[2] + " fechamentos e " + uploaded[3] + " itens de estoque enviados ao ERP.");
                ShowSync();
            }
            catch (WebException error)
            {
                state.ErpRefreshTokenProtected = ""; Save();
                string detail = "Falha de conexão com o ERP.";
                try { using (StreamReader reader = new StreamReader(error.Response.GetResponseStream())) detail = reader.ReadToEnd(); } catch { }
                MessageBox.Show(detail, "Sincronização ERP", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
            catch (Exception error)
            {
                MessageBox.Show(error.Message, "Sincronização ERP", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
            finally { Cursor.Current = previousCursor; }
        }

        private int[] UploadOperationalData(long companyId, string userId, string accessToken, List<ErpProduct> cloudProducts, List<ErpRevision> revisions)
        {
            string now = DateTime.UtcNow.ToString("o");
            long latestRevision = revisions == null || revisions.Count == 0 ? state.LastSyncRevision : revisions[0].id;
            List<ErpCreatedRecord> devices = ErpSyncClient.Send<List<ErpCreatedRecord>>(
                "/rest/v1/app_devices?on_conflict=company_id,device_uid",
                "POST",
                new object[] { new { company_id = companyId, device_uid = state.DeviceUid, name = Environment.MachineName, platform = "windows", app_version = "2.8.4", active = true, last_seen_at = now, last_sync_revision = latestRevision } },
                accessToken,
                "resolution=merge-duplicates,return=representation");
            if (devices == null || devices.Count == 0) throw new InvalidOperationException("Não foi possível registrar este computador no ERP.");

            List<object> tablePayload = new List<object>();
            for (int number = 1; number <= state.TableCount; number++) tablePayload.Add(new { company_id = companyId, number = number, name = "Mesa " + number.ToString("00"), sector = "Salão", seats = 4, active = true, updated_at = now });
            List<ErpTableRecord> cloudTables = ErpSyncClient.Send<List<ErpTableRecord>>(
                "/rest/v1/restaurant_tables?on_conflict=company_id,number",
                "POST", tablePayload.ToArray(), accessToken, "resolution=merge-duplicates,return=representation");
            Dictionary<int, long> tableIds = (cloudTables ?? new List<ErpTableRecord>()).ToDictionary(x => x.number, x => x.id);
            Dictionary<string, long> productIds = (cloudProducts ?? new List<ErpProduct>()).Where(x => !String.IsNullOrWhiteSpace(x.name)).GroupBy(x => x.name.Trim(), StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.First().id, StringComparer.OrdinalIgnoreCase);

            int uploaded = 0;
            foreach (Order order in state.Orders.OrderBy(x => x.Id))
            {
                string localId = order.Id.ToString(CultureInfo.InvariantCulture);
                int tableNumber = ParseTableNumber(order.Label);
                long tableId;
                string channel = OrderChannel(order.Label);
                string cloudStatus = OrderStatus(order.Status);
                object payload = new {
                    company_id = companyId,
                    table_id = tableNumber > 0 && tableIds.TryGetValue(tableNumber, out tableId) ? (long?)tableId : null,
                    channel = channel,
                    customer_name = order.Customer ?? "",
                    customer_phone = "",
                    status = cloudStatus,
                    subtotal = order.Subtotal,
                    service_fee = order.ServiceFee,
                    delivery_fee = 0,
                    discount = 0,
                    total = order.Total,
                    notes = order.CancellationReason ?? "",
                    device_id = state.DeviceUid,
                    local_id = localId,
                    opened_by = userId,
                    opened_at = String.IsNullOrWhiteSpace(order.CreatedAt) ? now : order.CreatedAt,
                    closed_at = String.IsNullOrWhiteSpace(order.ClosedAt) ? null : order.ClosedAt,
                    updated_at = now
                };
                List<ErpCreatedRecord> cloudOrders = ErpSyncClient.Send<List<ErpCreatedRecord>>(
                    "/rest/v1/orders?on_conflict=company_id,device_id,local_id",
                    "POST", new object[] { payload }, accessToken, "resolution=merge-duplicates,return=representation");
                if (cloudOrders == null || cloudOrders.Count == 0) continue;
                long cloudOrderId = cloudOrders[0].id;
                order.CloudOrderId = cloudOrderId;

                ErpSyncClient.Delete("/rest/v1/order_items?order_id=eq." + cloudOrderId, accessToken);
                List<object> items = new List<object>();
                foreach (OrderLine line in order.Lines ?? new List<OrderLine>())
                {
                    long productId;
                    items.Add(new { company_id = companyId, order_id = cloudOrderId, product_id = productIds.TryGetValue(line.Name ?? "", out productId) ? (long?)productId : null, product_name = line.Name ?? "Produto", quantity = line.Quantity, unit_price = line.UnitPrice, unit_cost = line.UnitCost, total = line.Total, status = String.IsNullOrWhiteSpace(line.ServedAt) ? "novo" : "servido", notes = "", created_at = String.IsNullOrWhiteSpace(line.CreatedAt) ? now : line.CreatedAt, local_id = String.IsNullOrWhiteSpace(line.LineId) ? Guid.NewGuid().ToString("N") : line.LineId });
                }
                if (items.Count > 0) ErpSyncClient.Send<object>("/rest/v1/order_items", "POST", items.ToArray(), accessToken, "return=minimal");

                ErpSyncClient.Delete("/rest/v1/payments?order_id=eq." + cloudOrderId, accessToken);
                List<object> payments = new List<object>();
                foreach (PaymentEntry payment in order.Payments ?? new List<PaymentEntry>()) payments.Add(new { company_id = companyId, order_id = cloudOrderId, method = PaymentMethod(payment.Method), amount = payment.Amount, status = "confirmado", paid_at = String.IsNullOrWhiteSpace(payment.CreatedAt) ? now : payment.CreatedAt, local_id = String.IsNullOrWhiteSpace(payment.LocalId) ? Guid.NewGuid().ToString("N") : payment.LocalId });
                if (payments.Count > 0) ErpSyncClient.Send<object>("/rest/v1/payments", "POST", payments.ToArray(), accessToken, "return=minimal");

                ErpSyncClient.Delete("/rest/v1/deliveries?order_id=eq." + cloudOrderId, accessToken);
                if (channel == "delivery") ErpSyncClient.Send<object>("/rest/v1/deliveries", "POST", new object[] { new { company_id = companyId, order_id = cloudOrderId, platform = "proprio", external_order_id = state.DeviceUid + "-" + localId, driver_name = "", address = "", status = DeliveryStatus(cloudStatus), delivered_at = cloudStatus == "pago" ? order.ClosedAt : null, updated_at = now } }, accessToken, "return=minimal");

                order.CloudSyncedAt = now;
                uploaded++;
            }
            int[] cashAndStock = UploadCashAndStock(companyId, userId, accessToken, now);
            return new int[] { uploaded, cashAndStock[0], cashAndStock[1], cashAndStock[2] };
        }

        private void TryUploadPaidOrders(bool showErrors)
        {
            try
            {
                string refreshToken = StateStore.UnprotectSecret(state.ErpRefreshTokenProtected);
                if (String.IsNullOrWhiteSpace(refreshToken)) return;

                ErpAuthSession session = ErpSyncClient.Refresh(refreshToken);
                if (session == null || String.IsNullOrWhiteSpace(session.access_token)) return;
                if (!String.IsNullOrWhiteSpace(session.refresh_token))
                    state.ErpRefreshTokenProtected = StateStore.ProtectSecret(session.refresh_token);

                ErpAuthUser user = session.user ?? ErpSyncClient.Get<ErpAuthUser>("/auth/v1/user", session.access_token);
                if (user == null || String.IsNullOrWhiteSpace(user.id)) return;

                List<ErpMember> memberships = ErpSyncClient.Get<List<ErpMember>>("/rest/v1/company_members?select=company_id&user_id=eq." + Uri.EscapeDataString(user.id) + "&active=eq.true&limit=1", session.access_token);
                if (memberships == null || memberships.Count == 0) return;
                long companyId = memberships[0].company_id;

                List<ErpProduct> products = ErpSyncClient.Get<List<ErpProduct>>("/rest/v1/products?select=id,category_id,sku,name,description,price,cost,unit,track_stock,active,revision&company_id=eq." + companyId + "&order=name.asc", session.access_token) ?? new List<ErpProduct>();
                List<ErpRevision> revisions = ErpSyncClient.Get<List<ErpRevision>>("/rest/v1/sync_revisions?select=id&company_id=eq." + companyId + "&order=id.desc&limit=1", session.access_token) ?? new List<ErpRevision>();
                UploadOperationalData(companyId, user.id, session.access_token, products, revisions);
                state.LastSyncAt = DateTime.Now.ToString("o");
                Save();
            }
            catch (Exception error)
            {
                if (showErrors) MessageBox.Show(error.Message, "Sincronização ERP", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }

        private int[] UploadCashAndStock(long companyId, string userId, string accessToken, string now)
        {
            int transactionCount = 0;
            int closureCount = 0;

            foreach (CashClosure closure in state.CashClosures ?? new List<CashClosure>())
            {
                string localId = String.IsNullOrWhiteSpace(closure.LocalId) ? Guid.NewGuid().ToString("N") : closure.LocalId;
                closure.LocalId = localId;
                string closedAt = String.IsNullOrWhiteSpace(closure.ClosedAt) ? now : closure.ClosedAt;
                ErpSyncClient.Send<object>(
                    "/rest/v1/cash_sessions?on_conflict=company_id,device_id,local_id",
                    "POST",
                    new object[] { new { company_id = companyId, device_id = state.DeviceUid, local_id = localId, opened_by = userId, opening_amount = 0, closing_amount = closure.NetTotal, status = "fechado", opened_at = closedAt, closed_at = closedAt, pix_amount = closure.Pix, cash_amount = closure.Cash, credit_amount = closure.Credit, debit_amount = closure.Debit, other_amount = closure.Other, expenses_amount = closure.Expenses } },
                    accessToken,
                    "resolution=merge-duplicates,return=minimal");
                closureCount++;
            }

            foreach (IGrouping<string, Transaction> day in (state.Transactions ?? new List<Transaction>()).GroupBy(x => TransactionDayKey(x.CreatedAt)))
            {
                string sessionLocalId = "movimentos-" + day.Key;
                List<ErpCreatedRecord> sessions = ErpSyncClient.Send<List<ErpCreatedRecord>>(
                    "/rest/v1/cash_sessions?on_conflict=company_id,device_id,local_id",
                    "POST",
                    new object[] { new { company_id = companyId, device_id = state.DeviceUid, local_id = sessionLocalId, opened_by = userId, opening_amount = 0, status = "aberto", opened_at = TransactionDayStart(day.Key, now) } },
                    accessToken,
                    "resolution=merge-duplicates,return=representation");
                if (sessions == null || sessions.Count == 0) continue;

                foreach (Transaction transaction in day)
                {
                    string localId = String.IsNullOrWhiteSpace(transaction.LocalId) ? Guid.NewGuid().ToString("N") : transaction.LocalId;
                    transaction.LocalId = localId;
                    ErpSyncClient.Send<object>(
                        "/rest/v1/cash_transactions?on_conflict=company_id,device_id,local_id",
                        "POST",
                        new object[] { new { company_id = companyId, cash_session_id = sessions[0].id, device_id = state.DeviceUid, local_id = localId, transaction_type = CashTransactionType(transaction), description = String.IsNullOrWhiteSpace(transaction.Description) ? "Movimentação de caixa" : transaction.Description, amount = Math.Abs(transaction.Amount), payment_method = PaymentMethod(transaction.Method), created_by = userId, created_at = String.IsNullOrWhiteSpace(transaction.CreatedAt) ? now : transaction.CreatedAt } },
                        accessToken,
                        "resolution=merge-duplicates,return=minimal");
                    transactionCount++;
                }
            }

            List<object> stock = new List<object>();
            foreach (InventoryItem item in state.Inventory ?? new List<InventoryItem>())
            {
                string localId = String.IsNullOrWhiteSpace(item.LocalId) ? Guid.NewGuid().ToString("N") : item.LocalId;
                item.LocalId = localId;
                stock.Add(new { company_id = companyId, device_id = state.DeviceUid, local_id = localId, name = String.IsNullOrWhiteSpace(item.Name) ? "Item sem nome" : item.Name.Trim(), unit = String.IsNullOrWhiteSpace(item.Unit) ? "un" : item.Unit.Trim(), quantity = item.Quantity, minimum_quantity = item.Minimum, unit_cost = item.UnitCost, updated_at = now });
            }
            if (stock.Count > 0)
                ErpSyncClient.Send<object>("/rest/v1/stock_materials?on_conflict=company_id,device_id,local_id", "POST", stock.ToArray(), accessToken, "resolution=merge-duplicates,return=minimal");

            return new int[] { transactionCount, closureCount, stock.Count };
        }

        private static string TransactionDayKey(string value)
        {
            DateTime parsed;
            if (!DateTime.TryParse(value, null, DateTimeStyles.RoundtripKind, out parsed)) parsed = DateTime.Now;
            return parsed.ToLocalTime().ToString("yyyyMMdd");
        }

        private static string TransactionDayStart(string key, string fallback)
        {
            DateTime parsed;
            return DateTime.TryParseExact(key, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed) ? parsed.ToUniversalTime().ToString("o") : fallback;
        }

        private static string CashTransactionType(Transaction transaction)
        {
            string description = (transaction.Description ?? "").ToLowerInvariant();
            if (description.Contains("despesa")) return "despesa";
            if (transaction.Amount < 0 || description.Contains("sangria") || description.Contains("retirada")) return "sangria";
            return "entrada";
        }

        private static int ParseTableNumber(string label)
        {
            Match match = Regex.Match(label ?? "", @"(?i)^mesa\s*(\d+)");
            int number; return match.Success && Int32.TryParse(match.Groups[1].Value, out number) ? number : 0;
        }

        private static string OrderChannel(string label)
        {
            string value = (label ?? "").Trim().ToLowerInvariant();
            if (value.StartsWith("mesa")) return "mesa";
            if (value.StartsWith("comanda")) return "comanda";
            if (value.Contains("delivery") || value.Contains("entrega")) return "delivery";
            if (value.Contains("balcão") || value.Contains("balcao")) return "balcao";
            return "aplicativo";
        }

        private static string OrderStatus(string status)
        {
            string value = (status ?? "").Trim().ToLowerInvariant();
            if (value == "pago") return "pago";
            if (value == "cancelado") return "cancelado";
            if (value.Contains("cozinha")) return "cozinha";
            if (value.Contains("pronto") || value.Contains("servido")) return "pronto";
            if (value.Contains("entrega")) return "entrega";
            return "aberto";
        }

        private static string DeliveryStatus(string orderStatus)
        {
            if (orderStatus == "pago") return "entregue";
            if (orderStatus == "entrega") return "rota";
            if (orderStatus == "pronto") return "pronto";
            if (orderStatus == "cancelado") return "cancelado";
            return "recebido";
        }

        private static string PaymentMethod(string method)
        {
            string value = (method ?? "outro").Trim().ToLowerInvariant();
            if (value.Contains("pix")) return "pix";
            if (value.Contains("dinheiro")) return "dinheiro";
            if (value.Contains("crédito") || value.Contains("credito")) return "credito";
            if (value.Contains("débito") || value.Contains("debito")) return "debito";
            return "outro";
        }

        private string SyncDateText(string value)
        {
            DateTime parsed;
            return DateTime.TryParse(value, null, DateTimeStyles.RoundtripKind, out parsed) ? parsed.ToLocalTime().ToString("dd/MM/yyyy HH:mm") : "data não informada";
        }

        private string DownloadsFolder()
        {
            string downloads = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
            if (Directory.Exists(downloads)) return downloads;
            string documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            return Directory.Exists(documents) ? documents : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
        }

        private void ImportLatestSyncPackage()
        {
            string folder = DownloadsFolder();
            string[] files = Directory.Exists(folder) ? Directory.GetFiles(folder, "*.json") : new string[0];
            string latest = files
                .Where(x => Path.GetFileName(x).IndexOf("ambrosia", StringComparison.OrdinalIgnoreCase) >= 0 || Path.GetFileName(x).IndexOf("erp", StringComparison.OrdinalIgnoreCase) >= 0)
                .OrderByDescending(File.GetLastWriteTime)
                .FirstOrDefault();
            if (String.IsNullOrWhiteSpace(latest))
            {
                Alert("Nenhum pacote do ERP foi encontrado em Downloads.\r\n\r\nNo ERP online, abra Administração → Sincronização, clique em Gerar pacote e aguarde o download. Depois volte aqui e clique novamente.");
                return;
            }
            ApplySyncPackage(latest);
        }

        private void ImportSyncPackage()
        {
            using (OpenFileDialog dialog = new OpenFileDialog())
            {
                dialog.Title = "Selecione o pacote gerado pelo ERP";
                dialog.Filter = "Arquivos JSON (*.json)|*.json|Todos os arquivos (*.*)|*.*";
                dialog.InitialDirectory = DownloadsFolder();
                dialog.RestoreDirectory = true;
                dialog.CheckFileExists = true;
                dialog.Multiselect = false;
                if (dialog.ShowDialog(this) != DialogResult.OK) return;
                ApplySyncPackage(dialog.FileName);
            }
        }

        private T DeserializeJsonInvariant<T>(JavaScriptSerializer serializer, string json)
        {
            CultureInfo previousCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
            CultureInfo previousUiCulture = System.Threading.Thread.CurrentThread.CurrentUICulture;
            try
            {
                System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
                System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
                try
                {
                    return serializer.Deserialize<T>(json);
                }
                catch (ArgumentException)
                {
                    byte[] bytes = Encoding.UTF8.GetBytes(json);
                    using (MemoryStream stream = new MemoryStream(bytes))
                    {
                        DataContractJsonSerializer robustSerializer = new DataContractJsonSerializer(typeof(T));
                        return (T)robustSerializer.ReadObject(stream);
                    }
                }
            }
            finally
            {
                System.Threading.Thread.CurrentThread.CurrentCulture = previousCulture;
                System.Threading.Thread.CurrentThread.CurrentUICulture = previousUiCulture;
            }
        }

        private void ApplySyncPackage(string fileName)
        {
            try
            {
                    if (String.IsNullOrWhiteSpace(fileName) || !File.Exists(fileName)) throw new FileNotFoundException("O pacote selecionado não foi encontrado.");
                    string json = File.ReadAllText(fileName, Encoding.UTF8).Trim();
                    if (json.StartsWith("\uFEFF")) json = json.Substring(1);
                    if (String.IsNullOrWhiteSpace(json)) throw new InvalidOperationException("O pacote do ERP está vazio.");
                    // Repara pacotes exportados pelo ERP sem as chaves externas do objeto JSON.
                    if (!json.StartsWith("{") && json.IndexOf("\"Format\"", StringComparison.OrdinalIgnoreCase) >= 0)
                        json = "{" + json.Trim().TrimEnd(',') + "}";

                    JavaScriptSerializer serializer = new JavaScriptSerializer();
                    serializer.MaxJsonLength = Int32.MaxValue;
                    SyncPackage package = DeserializeJsonInvariant<SyncPackage>(serializer, json);
                    if (package != null && String.IsNullOrWhiteSpace(package.Format))
                    {
                        AppState backup = DeserializeJsonInvariant<AppState>(serializer, json);
                        if (backup != null && backup.Products != null && backup.AppUsers != null)
                        {
                            package = new SyncPackage {
                                Format = "ambrosia-erp-sync", Version = 1,
                                Revision = Math.Max(1, state.LastSyncRevision + 1),
                                GeneratedAt = DateTime.Now.ToString("o"),
                                RestaurantName = backup.RestaurantName,
                                RestaurantSubtitle = backup.RestaurantSubtitle,
                                ReceiptFooter = backup.ReceiptFooter,
                                PrintFontSize = backup.PrintFontSize,
                                PrintFontName = backup.PrintFontName,
                                PaperWidth = backup.PaperWidth,
                                PrintCopies = backup.PrintCopies,
                                DefaultPaymentMethod = backup.DefaultPaymentMethod,
                                Products = backup.Products,
                                ProductCategories = backup.ProductCategories,
                                Inventory = backup.Inventory,
                                Employees = backup.Employees == null ? new List<SyncEmployee>() : backup.Employees.Select(x => new SyncEmployee { Name = x.Name, Role = x.Role, Active = true }).ToList(),
                                AppUsers = backup.AppUsers
                            };
                        }
                    }
                    if (package == null || !String.Equals(package.Format, "ambrosia-erp-sync", StringComparison.OrdinalIgnoreCase))
                        throw new InvalidOperationException("O arquivo selecionado não é um pacote de sincronização nem um backup válido da Ambrosia.");
                    if (package.Version != 1) throw new InvalidOperationException("Esta versão do pacote não é compatível com o aplicativo.");
                    if (package.AppUsers == null || !package.AppUsers.Any(x => x.Active && String.Equals(x.Role, "admin", StringComparison.OrdinalIgnoreCase)))
                        throw new InvalidOperationException("O pacote precisa manter pelo menos um administrador ativo.");

                    if (package.Products != null) state.Products = package.Products;
                    if (package.ProductCategories != null) state.ProductCategories = package.ProductCategories.Where(x => !String.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
                    if (package.Inventory != null) state.Inventory = package.Inventory;
                    if (package.Employees != null)
                    {
                        List<Employee> synchronizedEmployees = new List<Employee>();
                        foreach (SyncEmployee incoming in package.Employees.Where(x => x.Active))
                        {
                            Employee existing = state.Employees.FirstOrDefault(x => String.Equals(x.Name, incoming.Name, StringComparison.OrdinalIgnoreCase));
                            synchronizedEmployees.Add(new Employee {
                                Name = incoming.Name, Role = incoming.Role,
                                Working = existing != null && existing.Working,
                                ClockIn = existing == null ? "" : existing.ClockIn,
                                LastClockOut = existing == null ? "" : existing.LastClockOut
                            });
                        }
                        state.Employees = synchronizedEmployees;
                    }
                    state.AppUsers = package.AppUsers;
                    if (!String.IsNullOrWhiteSpace(package.RestaurantName)) state.RestaurantName = package.RestaurantName.Trim();
                    if (!String.IsNullOrWhiteSpace(package.RestaurantSubtitle)) state.RestaurantSubtitle = package.RestaurantSubtitle.Trim();
                    if (!String.IsNullOrWhiteSpace(package.ReceiptFooter)) state.ReceiptFooter = package.ReceiptFooter.Trim();
                    if (package.PaperWidth == 58 || package.PaperWidth == 80) state.PaperWidth = package.PaperWidth;
                    if (package.PrintCopies >= 1 && package.PrintCopies <= 3) state.PrintCopies = package.PrintCopies;
                    if (package.PrintFontSize == "Muito pequena" || package.PrintFontSize == "Pequena" || package.PrintFontSize == "Média" || package.PrintFontSize == "Grande" || package.PrintFontSize == "Muito grande") state.PrintFontSize = package.PrintFontSize;
                    if (package.PrintFontName == "Consolas" || package.PrintFontName == "Arial" || package.PrintFontName == "Courier New" || package.PrintFontName == "Segoe UI") state.PrintFontName = package.PrintFontName;
                    if (package.DefaultPaymentMethod == "Pix" || package.DefaultPaymentMethod == "Dinheiro" || package.DefaultPaymentMethod == "Crédito" || package.DefaultPaymentMethod == "Débito") state.DefaultPaymentMethod = package.DefaultPaymentMethod;
                    state.LastSyncAt = DateTime.Now.ToString("o");
                    state.LastSyncRevision = package.Revision;
                    state.SettingsVersion = 3;
                    Save();
                    Text = state.RestaurantName + " — Sistema Nativo 2.8.4";
                    Alert("Sincronização concluída.\r\n\r\nRevisão " + state.LastSyncRevision + " aplicada com sucesso. Mesas, pedidos e pagamentos locais foram preservados.");
                    ShowSync();
            }
            catch (Exception error)
            {
                MessageBox.Show(error.Message + "\r\n\r\nArquivo: " + Path.GetFileName(fileName), "Falha na sincronização", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }

        private void ShowSettings()
        {
            if (currentUserRole != "admin") { Alert("Acesso permitido somente ao Administrador."); ShowDashboard(); return; }
            FlowLayoutPanel page = StartPage("Configurações", "Personalize o restaurante, o atendimento, a impressão e os backups do aplicativo.");
            TabControl tabs = new TabControl { Width = 950, Height = 560, Font = new Font("Segoe UI", 9, FontStyle.Bold), Margin = new Padding(0, 12, 0, 8) };
            TabPage general = new TabPage("Geral") { BackColor = Theme.Paper };
            TabPage service = new TabPage("Atendimento") { BackColor = Theme.Paper };
            TabPage printing = new TabPage("Impressão") { BackColor = Theme.Paper };
            TabPage backup = new TabPage("Backup e dados") { BackColor = Theme.Paper };
            tabs.TabPages.Add(general); tabs.TabPages.Add(service); tabs.TabPages.Add(printing); tabs.TabPages.Add(backup);

            Label nameLabel = Theme.Label("Nome do restaurante", 9, FontStyle.Bold, Theme.Brown); nameLabel.Location = new Point(28, 28); general.Controls.Add(nameLabel);
            TextBox restaurantName = new TextBox { Location = new Point(30, 55), Width = 420, Text = state.RestaurantName }; general.Controls.Add(restaurantName);
            Label subtitleLabel = Theme.Label("Frase impressa no cabeçalho", 9, FontStyle.Bold, Theme.Brown); subtitleLabel.Location = new Point(28, 96); general.Controls.Add(subtitleLabel);
            TextBox restaurantSubtitle = new TextBox { Location = new Point(30, 123), Width = 620, Text = state.RestaurantSubtitle }; general.Controls.Add(restaurantSubtitle);
            Label footerLabel = Theme.Label("Mensagem no rodapé do comprovante", 9, FontStyle.Bold, Theme.Brown); footerLabel.Location = new Point(28, 164); general.Controls.Add(footerLabel);
            TextBox receiptFooter = new TextBox { Location = new Point(30, 191), Width = 620, Text = state.ReceiptFooter }; general.Controls.Add(receiptFooter);
            Label startLabel = Theme.Label("Tela inicial do aplicativo", 9, FontStyle.Bold, Theme.Brown); startLabel.Location = new Point(28, 232); general.Controls.Add(startLabel);
            ComboBox startScreen = new ComboBox { Location = new Point(30, 259), Width = 270, DropDownStyle = ComboBoxStyle.DropDownList }; startScreen.Items.AddRange(new object[] { "Início operacional" }); startScreen.SelectedIndex = 0; general.Controls.Add(startScreen);
            Label tableCountLabel = Theme.Label("Quantidade de mesas do estabelecimento", 9, FontStyle.Bold, Theme.Brown); tableCountLabel.Location = new Point(340, 232); general.Controls.Add(tableCountLabel);
            NumericUpDown tableCount = new NumericUpDown { Location = new Point(342, 259), Width = 120, Minimum = 1, Maximum = 100, Value = state.TableCount }; general.Controls.Add(tableCount);

            Label serviceTitle = Theme.Label("COMPORTAMENTO DO ATENDIMENTO", 10, FontStyle.Bold, Theme.Orange); serviceTitle.Location = new Point(28, 28); service.Controls.Add(serviceTitle);
            CheckBox autoPrint = new CheckBox { Text = "Também imprimir automaticamente comandas e balcão (mesas sempre imprimem)", Location = new Point(30, 70), Width = 650, Checked = state.AutoPrintOrder, Font = new Font("Segoe UI", 9) }; service.Controls.Add(autoPrint);
            CheckBox askReceipt = new CheckBox { Text = "Perguntar se deseja imprimir comprovante após o pagamento", Location = new Point(30, 108), Width = 550, Checked = state.AskPrintReceipt, Font = new Font("Segoe UI", 9) }; service.Controls.Add(askReceipt);
            CheckBox confirmPayment = new CheckBox { Text = "Pedir confirmação antes de registrar o pagamento", Location = new Point(30, 146), Width = 550, Checked = state.ConfirmBeforePayment, Font = new Font("Segoe UI", 9) }; service.Controls.Add(confirmPayment);
            Label paymentLabel = Theme.Label("Forma de pagamento padrão", 9, FontStyle.Bold, Theme.Brown); paymentLabel.Location = new Point(28, 198); service.Controls.Add(paymentLabel);
            ComboBox defaultPayment = new ComboBox { Location = new Point(30, 225), Width = 270, DropDownStyle = ComboBoxStyle.DropDownList }; defaultPayment.Items.AddRange(new object[] { "Pix", "Dinheiro", "Crédito", "Débito" }); defaultPayment.SelectedItem = state.DefaultPaymentMethod; if (defaultPayment.SelectedIndex < 0) defaultPayment.SelectedIndex = 0; service.Controls.Add(defaultPayment);

            Label printerLabel = Theme.Label("Impressora instalada no Windows", 9, FontStyle.Bold, Theme.Brown); printerLabel.Location = new Point(28, 28); printing.Controls.Add(printerLabel);
            ComboBox printers = new ComboBox { Location = new Point(30, 55), Width = 540, DropDownStyle = ComboBoxStyle.DropDownList };
            foreach (string printer in PrinterSettings.InstalledPrinters) printers.Items.Add(printer);
            if (!String.IsNullOrWhiteSpace(state.PrinterName) && printers.Items.Contains(state.PrinterName)) printers.SelectedItem = state.PrinterName; else if (printers.Items.Count > 0) printers.SelectedIndex = 0;
            printing.Controls.Add(printers);
            Label paperLabel = Theme.Label("Largura do papel", 9, FontStyle.Bold, Theme.Brown); paperLabel.Location = new Point(28, 102); printing.Controls.Add(paperLabel);
            ComboBox paperWidth = new ComboBox { Location = new Point(30, 129), Width = 180, DropDownStyle = ComboBoxStyle.DropDownList }; paperWidth.Items.AddRange(new object[] { "80 mm", "58 mm" }); paperWidth.SelectedItem = state.PaperWidth == 58 ? "58 mm" : "80 mm"; printing.Controls.Add(paperWidth);
            Label copiesLabel = Theme.Label("Número de vias", 9, FontStyle.Bold, Theme.Brown); copiesLabel.Location = new Point(245, 102); printing.Controls.Add(copiesLabel);
            NumericUpDown printCopies = new NumericUpDown { Location = new Point(247, 129), Width = 90, Minimum = 1, Maximum = 3, Value = state.PrintCopies }; printing.Controls.Add(printCopies);
            Label fontSizeLabel = Theme.Label("Tamanho da fonte", 9, FontStyle.Bold, Theme.Brown); fontSizeLabel.Location = new Point(370, 102); printing.Controls.Add(fontSizeLabel);
            ComboBox fontSize = new ComboBox { Location = new Point(372, 129), Width = 165, DropDownStyle = ComboBoxStyle.DropDownList }; fontSize.Items.AddRange(new object[] { "Muito pequena", "Pequena", "Média", "Grande", "Muito grande" }); fontSize.SelectedItem = state.PrintFontSize; if (fontSize.SelectedIndex < 0) fontSize.SelectedItem = "Média"; printing.Controls.Add(fontSize);
            Label fontNameLabel = Theme.Label("Tipo de fonte da impressão", 9, FontStyle.Bold, Theme.Brown); fontNameLabel.Location = new Point(28, 177); printing.Controls.Add(fontNameLabel);
            ComboBox fontName = new ComboBox { Location = new Point(30, 204), Width = 260, DropDownStyle = ComboBoxStyle.DropDownList }; fontName.Items.AddRange(new object[] { "Consolas", "Arial", "Courier New", "Segoe UI" }); fontName.SelectedItem = state.PrintFontName; if (fontName.SelectedIndex < 0) fontName.SelectedItem = "Consolas"; printing.Controls.Add(fontName);
            Label printHelp = Theme.Label("Pedidos com comida imprimem sempre 2 vias. Para a Elgin i8, use papel 80 mm. Confirme em Imprimir teste.", 9, FontStyle.Bold, Theme.Muted); printHelp.Location = new Point(28, 250); printHelp.MaximumSize = new Size(760, 0); printing.Controls.Add(printHelp);

            Label backupLabel = Theme.Label("Pasta onde os backups serão salvos", 9, FontStyle.Bold, Theme.Brown); backupLabel.Location = new Point(28, 28); backup.Controls.Add(backupLabel);
            TextBox backupFolder = new TextBox { Location = new Point(30, 55), Width = 620, Text = state.BackupFolder, ReadOnly = true }; backup.Controls.Add(backupFolder);
            Button chooseFolder = Theme.Button("Escolher pasta", delegate { using (FolderBrowserDialog folder = new FolderBrowserDialog()) { folder.SelectedPath = backupFolder.Text; if (folder.ShowDialog(this) == DialogResult.OK) backupFolder.Text = folder.SelectedPath; } }, false); chooseFolder.Location = new Point(670, 48); backup.Controls.Add(chooseFolder);
            Label dataPath = Theme.Label("Arquivo principal: " + StateStore.DataFile, 9, FontStyle.Regular, Theme.Muted); dataPath.Location = new Point(28, 103); dataPath.MaximumSize = new Size(780, 0); backup.Controls.Add(dataPath);
            Label backupHelp = Theme.Label("A atualização preserva seus dados. Mesmo assim, faça um backup antes de alterações importantes.", 9, FontStyle.Bold, Color.Firebrick); backupHelp.Location = new Point(28, 146); backupHelp.MaximumSize = new Size(760, 0); backup.Controls.Add(backupHelp);

            Action applySettings = delegate {
                state.RestaurantName = String.IsNullOrWhiteSpace(restaurantName.Text) ? "Ambrosia Espetaria" : restaurantName.Text.Trim();
                state.RestaurantSubtitle = restaurantSubtitle.Text.Trim(); state.ReceiptFooter = receiptFooter.Text.Trim();
                state.StartScreen = Convert.ToString(startScreen.SelectedItem); state.DefaultPaymentMethod = Convert.ToString(defaultPayment.SelectedItem);
                state.TableCount = Convert.ToInt32(tableCount.Value);
                state.AutoPrintOrder = autoPrint.Checked; state.AskPrintReceipt = askReceipt.Checked; state.ConfirmBeforePayment = confirmPayment.Checked;
                if (printers.SelectedItem != null) state.PrinterName = printers.SelectedItem.ToString();
                state.PaperWidth = Convert.ToString(paperWidth.SelectedItem).StartsWith("58") ? 58 : 80; state.PrintCopies = Convert.ToInt32(printCopies.Value); state.PrintFontSize = Convert.ToString(fontSize.SelectedItem); state.PrintFontName = Convert.ToString(fontName.SelectedItem);
                state.BackupFolder = String.IsNullOrWhiteSpace(backupFolder.Text) ? Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) : backupFolder.Text;
                state.SettingsVersion = 3; Save(); Text = state.RestaurantName + " — Sistema Nativo 2.8.4";
            };

            Button saveGeneral = Theme.Button("Salvar configurações", delegate { applySettings(); Alert("Configurações gerais salvas."); }, true); saveGeneral.Location = new Point(30, 320); saveGeneral.Width = 190; general.Controls.Add(saveGeneral);
            Button saveService = Theme.Button("Salvar configurações", delegate { applySettings(); Alert("Configurações de atendimento salvas."); }, true); saveService.Location = new Point(30, 286); saveService.Width = 190; service.Controls.Add(saveService);
            Button savePrint = Theme.Button("Salvar impressão", delegate { applySettings(); Alert("Configuração de fonte e impressão salva."); }, true); savePrint.Location = new Point(30, 306); savePrint.Width = 180; printing.Controls.Add(savePrint);
            Button testPrint = Theme.Button("Imprimir teste", delegate { applySettings(); if (String.IsNullOrWhiteSpace(state.PrinterName)) { Alert("Nenhuma impressora encontrada."); return; } Order test = new Order { Id = 0, Label = "TESTE DE IMPRESSÃO", Customer = state.RestaurantName, Status = "Teste", CreatedAt = DateTime.Now.ToString("o"), Lines = new List<OrderLine> { new OrderLine { Name = "Fonte " + state.PrintFontName + " · " + state.PrintFontSize + " · Papel " + state.PaperWidth + " mm", Quantity = 1, UnitPrice = 0 } } }; Print(test, false); }, false); testPrint.Location = new Point(228, 306); printing.Controls.Add(testPrint);
            Button saveBackupFolder = Theme.Button("Salvar pasta", delegate { applySettings(); Directory.CreateDirectory(state.BackupFolder); Alert("Pasta de backup salva."); }, true); saveBackupFolder.Location = new Point(30, 205); backup.Controls.Add(saveBackupFolder);
            Button makeBackup = Theme.Button("Criar backup agora", delegate { applySettings(); Directory.CreateDirectory(state.BackupFolder); string target = Path.Combine(state.BackupFolder, "Ambrosia-backup-" + DateTime.Now.ToString("yyyyMMdd-HHmm") + ".json"); File.Copy(StateStore.DataFile, target, true); Alert("Backup salvo em:\r\n" + target); }, false); makeBackup.Location = new Point(193, 205); makeBackup.Width = 170; backup.Controls.Add(makeBackup);
            Button openData = Theme.Button("Abrir pasta de dados", delegate { Process.Start("explorer.exe", StateStore.DataFolder); }, false); openData.Location = new Point(381, 205); openData.Width = 175; backup.Controls.Add(openData);

            page.Controls.Add(tabs);
            page.Controls.Add(Theme.Label("As configurações são salvas neste computador e continuam disponíveis sem internet.", 9, FontStyle.Bold, Theme.Muted));
        }

        private void Alert(string text) { MessageBox.Show(text, "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Information); }
    }

    public class TableServiceForm : Form
    {
        private Order order;
        private ComboBox products;
        private NumericUpDown quantity;
        private DataGridView grid;
        private Label total;
        private Label customer;
        private System.Windows.Forms.Timer liveClock;
        private Action<Product, decimal> addItemAction;
        private Func<OrderLine, decimal, string, bool> cancelItemAction;
        private Func<string, bool> cancelOrderAction;
        private Action markServedAction;
        private bool isTable;

        public TableServiceForm(Order currentOrder, List<Product> catalog, Action<Product, decimal> addAction, Func<OrderLine, decimal, string, bool> cancelLineAction, Func<string, bool> cancelWholeAction, Action markKitchenServedAction, Action partialAction, Action printAction, Func<bool> transferTableAction, Func<bool> receiveAction)
        {
            order = currentOrder; addItemAction = addAction; cancelItemAction = cancelLineAction; cancelOrderAction = cancelWholeAction; markServedAction = markKitchenServedAction;
            isTable = !String.IsNullOrWhiteSpace(order.Label) && order.Label.Trim().StartsWith("Mesa", StringComparison.OrdinalIgnoreCase);
            Text = order.Label + " — Atendimento"; Width = 900; Height = 710; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label(order.Label, 21, FontStyle.Bold, Theme.Brown); title.Location = new Point(24, 18); Controls.Add(title);
            customer = Theme.Label("", 9, FontStyle.Bold, Theme.Muted); customer.Location = new Point(26, 56); customer.Width = 500; Controls.Add(customer);
            total = Theme.Label((isTable ? "Total com 10%: " : "Total: ") + order.Total.ToString("C2"), 17, FontStyle.Bold, Theme.Orange); total.Location = new Point(570, 25); total.Width = 290; total.TextAlign = ContentAlignment.MiddleRight; Controls.Add(total);

            FlowLayoutPanel actions = new FlowLayoutPanel { Location = new Point(22, 82), Width = 836, Height = 48, WrapContents = false };
            Button partial = Theme.Button("Ver parcial", delegate { partialAction(); RefreshOrder(); }, false); partial.Width = 128; actions.Controls.Add(partial);
            Button printPartial = Theme.Button("Imprimir parcial", delegate { printAction(); }, false); printPartial.Width = 128; actions.Controls.Add(printPartial);
            Button served = Theme.Button("Marcar servido", delegate { markServedAction(); RefreshOrder(); }, false); served.Width = 128; actions.Controls.Add(served);
            Button closeAccount = Theme.Button("Fechar conta", delegate { if (receiveAction()) { DialogResult = DialogResult.OK; Close(); } else RefreshOrder(); }, true); closeAccount.Width = 128; actions.Controls.Add(closeAccount);
            Button cancelItem = Theme.Button("Cancelar item", delegate { CancelSelectedItem(); }, false); cancelItem.Width = 128; actions.Controls.Add(cancelItem);
            Button cancelOrder = Theme.Button(isTable ? "Cancelar mesa" : "Cancelar pedido", delegate { CancelWholeOrder(); }, false); cancelOrder.Width = 128; actions.Controls.Add(cancelOrder);
            Controls.Add(actions);

            Label current = Theme.Label(isTable ? "PRODUTOS DA MESA" : "PRODUTOS DO PEDIDO", 9, FontStyle.Bold, Theme.Orange); current.Location = new Point(26, 145); Controls.Add(current);
            grid = new DataGridView { Location = new Point(26, 173), Width = 832, Height = 285, BackgroundColor = Color.White, BorderStyle = BorderStyle.None, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false, RowHeadersVisible = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect, MultiSelect = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill };
            grid.EnableHeadersVisualStyles = false; grid.ColumnHeadersDefaultCellStyle.BackColor = Theme.Brown; grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.White; grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9, FontStyle.Bold); grid.RowTemplate.Height = 30;
            grid.Columns.Add("Time", "Tempo cozinha"); grid.Columns.Add("Product", "Produto"); grid.Columns.Add("Quantity", "Qtd."); grid.Columns.Add("Price", "Unitário"); grid.Columns.Add("Total", "Subtotal");
            grid.Columns[0].FillWeight = 92; grid.Columns[1].FillWeight = 180; grid.Columns[2].FillWeight = 48; grid.Columns[3].FillWeight = 70; grid.Columns[4].FillWeight = 75; Controls.Add(grid);
            grid.Columns[0].DefaultCellStyle.Font = new Font("Segoe UI", 9, FontStyle.Bold);
            Label cancelHint = Theme.Label("Selecione um produto e use Cancelar item. O motivo é obrigatório e o total é recalculado.", 8, FontStyle.Bold, Color.Firebrick); cancelHint.Location = new Point(26, 468); cancelHint.MaximumSize = new Size(790, 0); Controls.Add(cancelHint);

            Label addTitle = Theme.Label("ADICIONAR NOVOS PRODUTOS", 9, FontStyle.Bold, Theme.Orange); addTitle.Location = new Point(26, 514); Controls.Add(addTitle);
            products = new ComboBox { Location = new Point(26, 544), Width = 545, DropDownStyle = ComboBoxStyle.DropDownList, DataSource = catalog.Where(x => x.Active).OrderBy(x => x.Category).ThenBy(x => x.Name).ToList(), DisplayMember = "Display" }; Controls.Add(products);
            quantity = new NumericUpDown { Location = new Point(587, 544), Width = 75, Minimum = 1, Maximum = 50, Value = 1 }; Controls.Add(quantity);
            Button add = Theme.Button(isTable ? "Adicionar à mesa" : "Adicionar ao pedido", delegate { AddItem(); }, true); add.Location = new Point(678, 537); add.Width = 180; Controls.Add(add);
            Label note = Theme.Label("Os novos itens ficam marcados como Na cozinha e atualizam o estoque vinculado.", 8, FontStyle.Regular, Theme.Muted); note.Location = new Point(26, 582); Controls.Add(note);
            if (isTable)
            {
                Button transfer = Theme.Button("Transferir mesa", delegate { if (transferTableAction()) { DialogResult = DialogResult.OK; Close(); } }, false); transfer.Location = new Point(530, 605); transfer.Width = 165; Controls.Add(transfer);
            }
            Button close = Theme.Button("Fechar", delegate { Close(); }, false); close.Location = new Point(713, 605); Controls.Add(close);
            liveClock = new System.Windows.Forms.Timer { Interval = 1000 };
            liveClock.Tick += delegate { RefreshClock(); };
            liveClock.Start(); FormClosed += delegate { liveClock.Stop(); liveClock.Dispose(); };
            RefreshClock();
            RefreshOrder();
        }

        private void RefreshClock()
        {
            string customerName = String.IsNullOrWhiteSpace(order.Customer) ? "Cliente não informado" : order.Customer;
            if (String.IsNullOrWhiteSpace(order.KitchenStartedAt))
            {
                customer.Text = customerName + "   ·   SEM COMIDA PENDENTE";
                customer.ForeColor = Theme.Muted;
            }
            else
            {
                customer.Text = customerName + "   ·   COMIDA MAIS ANTIGA " + MainForm.ElapsedText(order.KitchenStartedAt);
                customer.ForeColor = MainForm.KitchenColorFor(order.KitchenStartedAt);
            }
            foreach (DataGridViewRow row in grid.Rows)
            {
                OrderLine line = row.Tag as OrderLine; if (line == null) continue;
                if (!StateStore.LineIsFood(line))
                {
                    row.Cells["Time"].Value = "—"; row.Cells["Time"].Style.ForeColor = Theme.Muted;
                }
                else if (!String.IsNullOrWhiteSpace(line.ServedAt))
                {
                    row.Cells["Time"].Value = "Servido " + MainForm.ElapsedTextUntil(line.CreatedAt, line.ServedAt); row.Cells["Time"].Style.ForeColor = Color.FromArgb(45, 115, 87);
                }
                else
                {
                    row.Cells["Time"].Value = MainForm.ElapsedText(line.CreatedAt); row.Cells["Time"].Style.ForeColor = MainForm.KitchenColorFor(line.CreatedAt);
                }
            }
        }

        private void AddItem()
        {
            Product product = products.SelectedItem as Product; if (product == null) return;
            addItemAction(product, quantity.Value); RefreshOrder();
        }

        private void CancelSelectedItem()
        {
            if (grid.SelectedRows.Count == 0 || grid.SelectedRows[0].Tag == null) { MessageBox.Show("Selecione o produto que deseja cancelar.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
            OrderLine line = grid.SelectedRows[0].Tag as OrderLine; if (line == null) return;
            using (CancelItemForm cancel = new CancelItemForm(line))
            {
                if (cancel.ShowDialog(this) == DialogResult.OK && cancelItemAction(line, cancel.CancelQuantity, cancel.Reason))
                {
                    MessageBox.Show("Item cancelado. A conta foi recalculada.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Information); RefreshOrder();
                }
            }
        }

        private void CancelWholeOrder()
        {
            using (CancelOrderForm cancel = new CancelOrderForm(order.Label))
            {
                if (cancel.ShowDialog(this) == DialogResult.OK && cancelOrderAction(cancel.Reason))
                {
                    MessageBox.Show(isTable ? "Mesa cancelada e liberada." : "Pedido cancelado.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Information); DialogResult = DialogResult.OK; Close();
                }
            }
        }

        private void RefreshOrder()
        {
            grid.Rows.Clear();
            if (order.Lines != null) foreach (OrderLine line in order.Lines) { int row = grid.Rows.Add("", line.Name, line.Quantity.ToString("0.##"), line.UnitPrice.ToString("C2"), line.Total.ToString("C2")); grid.Rows[row].Tag = line; }
            total.Text = (isTable ? "Total com 10%: " : "Total: ") + order.Total.ToString("C2");
            RefreshClock();
        }
    }

    public class PartialAccountForm : Form
    {
        public PartialAccountForm(Order order, Action printAction)
        {
            bool isTable = !String.IsNullOrWhiteSpace(order.Label) && order.Label.Trim().StartsWith("Mesa", StringComparison.OrdinalIgnoreCase);
            Text = "Parcial da conta — " + order.Label; Width = 700; Height = 650; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label("Parcial da conta", 19, FontStyle.Bold, Theme.Brown); title.Location = new Point(24, 20); Controls.Add(title);
            Label table = Theme.Label(order.Label + (String.IsNullOrWhiteSpace(order.Customer) ? "" : " · " + order.Customer), 10, FontStyle.Bold, Theme.Orange); table.Location = new Point(26, 58); Controls.Add(table);
            Label open = Theme.Label(isTable ? "● MESA CONTINUA EM ATENDIMENTO" : "● COMANDA CONTINUA ABERTA", 8, FontStyle.Bold, Color.FromArgb(45, 115, 87)); open.Location = new Point(26, 88); Controls.Add(open);

            DataGridView grid = new DataGridView { Location = new Point(26, 124), Width = 632, Height = 320, BackgroundColor = Color.White, BorderStyle = BorderStyle.None, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill };
            grid.EnableHeadersVisualStyles = false; grid.ColumnHeadersDefaultCellStyle.BackColor = Theme.Brown; grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.White; grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9, FontStyle.Bold); grid.RowTemplate.Height = 30;
            grid.Columns.Add("Product", "Item"); grid.Columns.Add("Quantity", "Qtd."); grid.Columns.Add("Price", "Unitário"); grid.Columns.Add("Total", "Subtotal");
            grid.Columns[0].FillWeight = 190; grid.Columns[1].FillWeight = 50; grid.Columns[2].FillWeight = 75; grid.Columns[3].FillWeight = 80;
            foreach (OrderLine line in order.Lines) grid.Rows.Add(line.Name, line.Quantity.ToString("0.##"), line.UnitPrice.ToString("C2"), line.Total.ToString("C2"));
            Controls.Add(grid);

            Label subtotal = Theme.Label("Subtotal do consumo: " + order.Subtotal.ToString("C2"), 9, FontStyle.Bold, Theme.Muted); subtotal.Location = new Point(330, 454); subtotal.Width = 330; subtotal.TextAlign = ContentAlignment.MiddleRight; Controls.Add(subtotal);
            Label fee = Theme.Label("Taxa de atendimento (10%): " + order.ServiceFee.ToString("C2"), 9, FontStyle.Bold, Theme.Muted); fee.Location = new Point(330, 476); fee.Width = 330; fee.TextAlign = ContentAlignment.MiddleRight; Controls.Add(fee);
            Label totalLabel = Theme.Label(order.PaidAmount > 0 ? "FALTA RECEBER" : "TOTAL PARCIAL", 9, FontStyle.Bold, Theme.Muted); totalLabel.Location = new Point(382, 505); Controls.Add(totalLabel);
            Label total = Theme.Label((order.PaidAmount > 0 ? order.Remaining : order.Total).ToString("C2"), 21, FontStyle.Bold, Theme.Brown); total.Location = new Point(500, 496); Controls.Add(total);
            Label warning = Theme.Label(isTable ? "A mesa continua aberta até o pagamento total." : "A comanda continua aberta até o pagamento total.", 8, FontStyle.Bold, Color.Firebrick); warning.Location = new Point(26, 545); warning.MaximumSize = new Size(330, 0); Controls.Add(warning);
            Button print = Theme.Button("Imprimir parcial", delegate { printAction(); }, false); print.Location = new Point(360, 536); print.Width = 145; Controls.Add(print);
            Button close = Theme.Button("Fechar parcial", delegate { Close(); }, true); close.Location = new Point(513, 536); Controls.Add(close);
        }
    }

    public class PaymentForm : Form
    {
        private Order order;
        private ComboBox methods;
        private NumericUpDown amount;
        private DataGridView grid;
        private Label received;
        private Label remaining;
        private Button add;
        private Func<string, decimal, bool> register;

        public PaymentForm(Order currentOrder, string[] paymentMethods, Func<string, decimal, bool> registerPayment)
        {
            order = currentOrder; register = registerPayment;
            Text = "Fechar conta — " + order.Label; Width = 690; Height = 610; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label("Fechar conta", 20, FontStyle.Bold, Theme.Brown); title.Location = new Point(24, 18); Controls.Add(title);
            Label hint = Theme.Label("Registre cada valor e a forma de pagamento usada pelo cliente.", 9, FontStyle.Regular, Theme.Muted); hint.Location = new Point(26, 56); Controls.Add(hint);

            Panel totals = new Panel { Location = new Point(24, 88), Width = 632, Height = 92, BackColor = Color.FromArgb(255, 244, 237) };
            Label total = Theme.Label("TOTAL DA CONTA\r\n" + order.Total.ToString("C2"), 11, FontStyle.Bold, Theme.Brown); total.Location = new Point(18, 15); total.Width = 190; totals.Controls.Add(total);
            received = Theme.Label("JÁ RECEBIDO\r\n" + order.PaidAmount.ToString("C2"), 11, FontStyle.Bold, Theme.Brown); received.Location = new Point(225, 15); received.Width = 170; totals.Controls.Add(received);
            remaining = Theme.Label("FALTA RECEBER\r\n" + order.Remaining.ToString("C2"), 11, FontStyle.Bold, Theme.Orange); remaining.Location = new Point(420, 15); remaining.Width = 190; totals.Controls.Add(remaining); Controls.Add(totals);
            Label detail = Theme.Label("Consumo " + order.Subtotal.ToString("C2") + " + taxa de atendimento (10%) " + order.ServiceFee.ToString("C2"), 8, FontStyle.Bold, Theme.Muted); detail.Location = new Point(28, 187); detail.Width = 620; Controls.Add(detail);

            Label methodLabel = Theme.Label("FORMA DE PAGAMENTO", 8, FontStyle.Bold, Theme.Muted); methodLabel.Location = new Point(26, 222); Controls.Add(methodLabel);
            methods = new ComboBox { Location = new Point(26, 246), Width = 245, DropDownStyle = ComboBoxStyle.DropDownList }; methods.Items.AddRange(paymentMethods); if (methods.Items.Count > 0) methods.SelectedIndex = 0; Controls.Add(methods);
            Label amountLabel = Theme.Label("VALOR RECEBIDO", 8, FontStyle.Bold, Theme.Muted); amountLabel.Location = new Point(290, 222); Controls.Add(amountLabel);
            amount = new NumericUpDown { Location = new Point(290, 246), Width = 160, DecimalPlaces = 2, Minimum = 0.01m, Maximum = 100000m, Increment = 1m, ThousandsSeparator = true }; Controls.Add(amount);
            add = Theme.Button("Registrar pagamento", delegate { AddPayment(); }, true); add.Location = new Point(468, 238); add.Width = 188; Controls.Add(add);

            Label history = Theme.Label("PAGAMENTOS REGISTRADOS", 8, FontStyle.Bold, Theme.Orange); history.Location = new Point(26, 300); Controls.Add(history);
            grid = new DataGridView { Location = new Point(26, 326), Width = 630, Height = 164, BackgroundColor = Color.White, BorderStyle = BorderStyle.None, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill };
            grid.EnableHeadersVisualStyles = false; grid.ColumnHeadersDefaultCellStyle.BackColor = Theme.Brown; grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.White; grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9, FontStyle.Bold); grid.RowTemplate.Height = 28;
            grid.Columns.Add("Method", "Forma"); grid.Columns.Add("Amount", "Valor"); grid.Columns.Add("Time", "Horário"); Controls.Add(grid);
            Label note = Theme.Label("A mesa só será liberada quando o saldo chegar a R$ 0,00.", 8, FontStyle.Bold, Theme.Muted); note.Location = new Point(26, 507); note.Width = 430; Controls.Add(note);
            Button close = Theme.Button("Voltar para a mesa", delegate { Close(); }, false); close.Location = new Point(490, 500); close.Width = 166; Controls.Add(close);
            RefreshView();
        }

        private void AddPayment()
        {
            if (methods.SelectedItem == null || order.Remaining <= 0) return;
            decimal value = amount.Value;
            if (value > order.Remaining + 0.005m) { MessageBox.Show("O valor é maior que o saldo restante.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
            if (!register(Convert.ToString(methods.SelectedItem), value)) { MessageBox.Show("Não foi possível registrar este pagamento.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
            RefreshView();
            if (order.Status == "Pago") { DialogResult = DialogResult.OK; Close(); }
        }

        private void RefreshView()
        {
            received.Text = "JÁ RECEBIDO\r\n" + order.PaidAmount.ToString("C2");
            remaining.Text = "FALTA RECEBER\r\n" + order.Remaining.ToString("C2");
            grid.Rows.Clear();
            foreach (PaymentEntry payment in order.Payments) grid.Rows.Add(payment.Method, payment.Amount.ToString("C2"), DateTime.Parse(payment.CreatedAt).ToLocalTime().ToString("HH:mm"));
            bool open = order.Remaining > 0.005m;
            methods.Enabled = open; amount.Enabled = open; add.Enabled = open;
            if (open)
            {
                amount.Maximum = Math.Max(0.01m, order.Remaining);
                amount.Value = order.Remaining;
            }
        }
    }

    public class NewOrderForm : Form
    {
        private List<OrderLine> cart = new List<OrderLine>();
        private ComboBox products; private NumericUpDown quantity; private DataGridView grid; private Label total;
        private TextBox label; private TextBox customer; private int id;
        public Order CreatedOrder { get; private set; }

        public NewOrderForm(List<Product> catalog, int nextId) : this(catalog, nextId, "") { }

        public NewOrderForm(List<Product> catalog, int nextId, string fixedLabel)
        {
            bool hasFixedLabel = !String.IsNullOrWhiteSpace(fixedLabel);
            bool isTable = hasFixedLabel && fixedLabel.StartsWith("Mesa", StringComparison.OrdinalIgnoreCase);
            bool isCounter = hasFixedLabel && fixedLabel.StartsWith("Balcão", StringComparison.OrdinalIgnoreCase);
            string formTitle = isTable ? "Abrir " + fixedLabel : (isCounter ? "Nova venda no " + fixedLabel : "Nova comanda");
            id = nextId; Text = formTitle; Width = 820; Height = 620; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Segoe UI", 9);
            Label title = Theme.Label(formTitle, 19, FontStyle.Bold, Theme.Brown); title.Location = new Point(24, 20); Controls.Add(title);
            label = new TextBox { Location = new Point(26, 65), Width = 230, ReadOnly = hasFixedLabel }; label.Text = hasFixedLabel ? fixedLabel : "Comanda " + nextId.ToString("000"); Controls.Add(label);
            customer = new TextBox { Location = new Point(270, 65), Width = 230 }; Controls.Add(customer);
            Label labelHint = Theme.Label("Mesa / comanda", 8, FontStyle.Regular, Theme.Muted); labelHint.Location = new Point(26, 92); Controls.Add(labelHint);
            Label customerHint = Theme.Label("Cliente (opcional)", 8, FontStyle.Regular, Theme.Muted); customerHint.Location = new Point(270, 92); Controls.Add(customerHint);
            products = new ComboBox { Location = new Point(26, 126), Width = 474, DropDownStyle = ComboBoxStyle.DropDownList, DataSource = catalog.Where(x => x.Active).OrderBy(x => x.Category).ThenBy(x => x.Name).ToList(), DisplayMember = "Display" }; Controls.Add(products);
            quantity = new NumericUpDown { Location = new Point(516, 126), Width = 75, Minimum = 1, Maximum = 50, Value = 1 }; Controls.Add(quantity);
            Button add = Theme.Button("Adicionar", delegate { AddItem(); }, true); add.Location = new Point(606, 120); Controls.Add(add);
            grid = new DataGridView { Location = new Point(26, 180), Width = 742, Height = 310, BackgroundColor = Color.White, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill };
            grid.Columns.Add("Product", "Produto"); grid.Columns.Add("Qty", "Qtd."); grid.Columns.Add("Price", "Unitário"); grid.Columns.Add("Total", "Total"); Controls.Add(grid);
            Button remove = Theme.Button("Remover item", delegate { RemoveItem(); }, false); remove.Location = new Point(26, 505); Controls.Add(remove);
            total = Theme.Label("Total: R$ 0,00", 14, FontStyle.Bold, Theme.Brown); total.Location = new Point(445, 514); Controls.Add(total);
            Button save = Theme.Button(isTable ? "Abrir mesa" : (isCounter ? "Finalizar balcão" : "Criar comanda"), delegate { SaveOrder(); }, true); save.Location = new Point(623, 504); Controls.Add(save);
        }

        private void AddItem()
        {
            Product product = products.SelectedItem as Product; if (product == null) return;
            OrderLine existing = cart.FirstOrDefault(x => x.Name == product.Name);
            if (existing == null) cart.Add(new OrderLine { Name = product.Name, Quantity = quantity.Value, UnitPrice = product.Price, UnitCost = product.Cost }); else existing.Quantity += quantity.Value;
            RefreshCart();
        }
        private void RemoveItem() { if (grid.SelectedRows.Count == 0) return; int index = grid.SelectedRows[0].Index; if (index >= 0 && index < cart.Count) cart.RemoveAt(index); RefreshCart(); }
        private void RefreshCart() { grid.Rows.Clear(); foreach (OrderLine line in cart) grid.Rows.Add(line.Name, line.Quantity.ToString("0.##"), line.UnitPrice.ToString("C2"), line.Total.ToString("C2")); total.Text = "Total: " + cart.Sum(x => x.Total).ToString("C2"); }
        private void SaveOrder()
        {
            if (String.IsNullOrWhiteSpace(label.Text) || cart.Count == 0) { MessageBox.Show("Informe a identificação e adicione ao menos um produto.", "Ambrosia"); return; }
            CreatedOrder = new Order { Id = id, Label = label.Text.Trim(), Customer = customer.Text.Trim(), Status = "Na cozinha", CreatedAt = DateTime.Now.ToString("o"), Lines = cart }; DialogResult = DialogResult.OK; Close();
        }
    }

    public class CancelItemForm : Form
    {
        private NumericUpDown quantity;
        private TextBox reason;
        public decimal CancelQuantity { get { return quantity.Value; } }
        public string Reason { get { return reason.Text.Trim(); } }

        public CancelItemForm(OrderLine line)
        {
            Text = "Cancelar item"; Width = 470; Height = 310; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label("Cancelar item lançado", 17, FontStyle.Bold, Theme.Brown); title.Location = new Point(22, 18); Controls.Add(title);
            Label item = Theme.Label(line.Name + " · disponível " + line.Quantity.ToString("0.##"), 9, FontStyle.Bold, Theme.Orange); item.Location = new Point(24, 58); item.MaximumSize = new Size(400, 0); Controls.Add(item);
            Label quantityLabel = Theme.Label("Quantidade a cancelar", 8, FontStyle.Bold, Theme.Muted); quantityLabel.Location = new Point(24, 96); Controls.Add(quantityLabel);
            quantity = new NumericUpDown { Location = new Point(26, 120), Width = 125, Minimum = 0.01m, Maximum = line.Quantity, Value = line.Quantity, DecimalPlaces = 2, Increment = 1m }; Controls.Add(quantity);
            Label reasonLabel = Theme.Label("Motivo obrigatório", 8, FontStyle.Bold, Theme.Muted); reasonLabel.Location = new Point(24, 157); Controls.Add(reasonLabel);
            reason = new TextBox { Location = new Point(26, 181), Width = 395, Height = 45, Multiline = true, MaxLength = 180 }; Controls.Add(reason);
            Button confirm = Theme.Button("Confirmar cancelamento", delegate { if (String.IsNullOrWhiteSpace(reason.Text)) { MessageBox.Show("Informe o motivo do cancelamento.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } DialogResult = DialogResult.OK; Close(); }, true); confirm.Location = new Point(242, 232); confirm.Width = 180; Controls.Add(confirm); AcceptButton = confirm;
        }
    }

    public class CancelOrderForm : Form
    {
        private TextBox reason;
        public string Reason { get { return reason.Text.Trim(); } }

        public CancelOrderForm(string label)
        {
            Text = "Cancelar " + label; Width = 500; Height = 280; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label("Cancelar " + label, 17, FontStyle.Bold, Theme.Brown); title.Location = new Point(22, 18); Controls.Add(title);
            Label warning = Theme.Label("A mesa será liberada e os itens vinculados serão devolvidos ao estoque. Esta ação fica registrada.", 9, FontStyle.Bold, Color.Firebrick); warning.Location = new Point(24, 58); warning.MaximumSize = new Size(430, 0); Controls.Add(warning);
            Label reasonLabel = Theme.Label("Motivo obrigatório", 8, FontStyle.Bold, Theme.Muted); reasonLabel.Location = new Point(24, 112); Controls.Add(reasonLabel);
            reason = new TextBox { Location = new Point(26, 136), Width = 425, Height = 45, Multiline = true, MaxLength = 180 }; Controls.Add(reason);
            Button confirm = Theme.Button("Confirmar cancelamento", delegate { if (String.IsNullOrWhiteSpace(reason.Text)) { MessageBox.Show("Informe o motivo do cancelamento.", "Ambrosia", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (MessageBox.Show("Confirma o cancelamento de " + label + "?", "Ambrosia", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; DialogResult = DialogResult.OK; Close(); }, true); confirm.Location = new Point(272, 190); confirm.Width = 180; Controls.Add(confirm); AcceptButton = confirm;
        }
    }

    public class UserEditorForm : Form
    {
        private TextBox displayName;
        private TextBox userName;
        private TextBox password;
        private ComboBox role;
        public string DisplayName { get; private set; }
        public string UserName { get; private set; }
        public string Password { get; private set; }
        public string UserRole { get; private set; }

        public UserEditorForm(AppUser user)
        {
            bool editing = user != null; bool master = editing && String.Equals(user.Username, "ambrosia adm", StringComparison.OrdinalIgnoreCase);
            Text = editing ? "Editar usuário" : "Cadastrar usuário"; Width = 520; Height = 445; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper; Font = new Font("Arial", 10); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label(editing ? "EDITAR USUÁRIO E PERMISSÕES" : "NOVO USUÁRIO E PERMISSÕES", 15, FontStyle.Bold, Theme.Brown); title.Location = new Point(28, 20); Controls.Add(title);
            Controls.Add(new Label { Text = "NOME DO FUNCIONÁRIO", Location = new Point(30, 70), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            displayName = new TextBox { Location = new Point(32, 92), Width = 440, Text = editing ? user.DisplayName : "" }; Controls.Add(displayName);
            Controls.Add(new Label { Text = "USUÁRIO PARA ENTRAR", Location = new Point(30, 132), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            userName = new TextBox { Location = new Point(32, 154), Width = 440, Text = editing ? user.Username : "", ReadOnly = master }; Controls.Add(userName);
            Controls.Add(new Label { Text = master ? "SENHA MESTRE PROTEGIDA" : (editing ? "NOVA SENHA (DEIXE VAZIO PARA MANTER)" : "SENHA"), Location = new Point(30, 194), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            password = new TextBox { Location = new Point(32, 216), Width = 440, UseSystemPasswordChar = true, Enabled = !master }; Controls.Add(password);
            Controls.Add(new Label { Text = "FUNÇÃO E PERMISSÃO", Location = new Point(30, 256), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            role = new ComboBox { Location = new Point(32, 278), Width = 300, DropDownStyle = ComboBoxStyle.DropDownList }; role.Items.AddRange(new object[] { "Administrador", "Atendimento", "Caixa" });
            string selectedRole = editing && String.Equals(user.Role, "admin", StringComparison.OrdinalIgnoreCase) ? "Administrador" : (editing && String.Equals(user.Role, "caixa", StringComparison.OrdinalIgnoreCase) ? "Caixa" : "Atendimento"); role.SelectedItem = selectedRole; role.Enabled = !master; Controls.Add(role);
            Label permission = Theme.Label("Administrador: tudo  •  Atendimento: mesas e produtos  •  Caixa: somente caixa", 8, FontStyle.Bold, Theme.Orange); permission.Location = new Point(32, 316); Controls.Add(permission);
            Button cancel = Theme.Button("Cancelar", delegate { DialogResult = DialogResult.Cancel; Close(); }, false); cancel.Location = new Point(220, 350); cancel.Width = 115; Controls.Add(cancel);
            Button save = Theme.Button("Salvar usuário", delegate { Confirm(editing); }, true); save.Location = new Point(350, 350); save.Width = 140; Controls.Add(save); AcceptButton = save;
        }

        private void Confirm(bool editing)
        {
            if (String.IsNullOrWhiteSpace(displayName.Text)) { MessageBox.Show("Informe o nome do funcionário.", "Ambrosia"); return; }
            if (String.IsNullOrWhiteSpace(userName.Text)) { MessageBox.Show("Informe o usuário para entrar.", "Ambrosia"); return; }
            if (!editing && String.IsNullOrWhiteSpace(password.Text)) { MessageBox.Show("Informe a senha do novo usuário.", "Ambrosia"); return; }
            DisplayName = displayName.Text.Trim(); UserName = userName.Text.Trim(); Password = password.Text; UserRole = Convert.ToString(role.SelectedItem) == "Administrador" ? "admin" : (Convert.ToString(role.SelectedItem) == "Caixa" ? "caixa" : "atendimento");
            DialogResult = DialogResult.OK; Close();
        }
    }

    public class ProductEditorForm : Form
    {
        private TextBox productName;
        private ComboBox category;
        private TextBox price;
        private TextBox cost;
        public string EditedProductName { get; private set; }
        public string ProductCategory { get; private set; }
        public decimal ProductPrice { get; private set; }
        public decimal ProductCost { get; private set; }

        public ProductEditorForm(Product product, List<string> categories)
        {
            bool editing = product != null;
            Text = editing ? "Editar produto" : "Cadastrar produto e categoria";
            Width = 520; Height = 390; StartPosition = FormStartPosition.CenterParent; BackColor = Theme.Paper;
            Font = new Font("Arial", 10); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label(editing ? "EDITAR PRODUTO" : "NOVO PRODUTO E CATEGORIA", 16, FontStyle.Bold, Theme.Brown); title.Location = new Point(28, 22); Controls.Add(title);
            Label help = Theme.Label("Escolha uma categoria existente ou digite uma nova.", 9, FontStyle.Bold, Theme.Orange); help.Location = new Point(30, 60); Controls.Add(help);
            Controls.Add(new Label { Text = "NOME DO PRODUTO", Location = new Point(30, 98), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            productName = new TextBox { Location = new Point(32, 120), Width = 440, Text = editing ? product.Name : "" }; Controls.Add(productName);
            Controls.Add(new Label { Text = "CATEGORIA", Location = new Point(30, 158), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            category = new ComboBox { Location = new Point(32, 180), Width = 440, DropDownStyle = ComboBoxStyle.DropDown, AutoCompleteMode = AutoCompleteMode.SuggestAppend, AutoCompleteSource = AutoCompleteSource.ListItems };
            foreach (string item in categories.OrderBy(x => x)) category.Items.Add(item);
            category.Text = editing ? product.Category : (category.Items.Count > 0 ? Convert.ToString(category.Items[0]) : "Geral"); Controls.Add(category);
            Controls.Add(new Label { Text = "PREÇO DE VENDA", Location = new Point(30, 220), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            price = new TextBox { Location = new Point(32, 242), Width = 205, Text = editing ? product.Price.ToString("0.00") : "0,00" }; Controls.Add(price);
            Controls.Add(new Label { Text = "CUSTO", Location = new Point(265, 220), AutoSize = true, Font = new Font("Arial", 9, FontStyle.Bold) });
            cost = new TextBox { Location = new Point(267, 242), Width = 205, Text = editing ? product.Cost.ToString("0.00") : "0,00" }; Controls.Add(cost);
            Button cancel = Theme.Button("Cancelar", delegate { DialogResult = DialogResult.Cancel; Close(); }, false); cancel.Location = new Point(220, 292); cancel.Width = 115; Controls.Add(cancel);
            Button save = Theme.Button(editing ? "Salvar alterações" : "Cadastrar produto", delegate { Confirm(); }, true); save.Location = new Point(350, 292); save.Width = 140; Controls.Add(save); AcceptButton = save;
        }

        private void Confirm()
        {
            decimal parsedPrice; decimal parsedCost;
            if (String.IsNullOrWhiteSpace(productName.Text)) { MessageBox.Show("Informe o nome do produto.", "Ambrosia"); return; }
            if (String.IsNullOrWhiteSpace(category.Text)) { MessageBox.Show("Escolha ou digite uma categoria.", "Ambrosia"); return; }
            if (!Decimal.TryParse(price.Text, out parsedPrice) || parsedPrice < 0) { MessageBox.Show("Preço de venda inválido.", "Ambrosia"); return; }
            if (!Decimal.TryParse(cost.Text, out parsedCost) || parsedCost < 0) { MessageBox.Show("Custo inválido.", "Ambrosia"); return; }
            EditedProductName = productName.Text.Trim(); ProductCategory = category.Text.Trim(); ProductPrice = parsedPrice; ProductCost = parsedCost;
            DialogResult = DialogResult.OK; Close();
        }
    }

    public class ChoiceForm : Form
    {
        private ComboBox combo; public string Value { get { return combo.SelectedItem == null ? "" : combo.SelectedItem.ToString(); } }
        public ChoiceForm(string title, string label, string[] choices)
        {
            Text = title; Width = 390; Height = 190; StartPosition = FormStartPosition.CenterParent; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Controls.Add(new Label { Text = label, Location = new Point(20, 20), AutoSize = true }); combo = new ComboBox { Location = new Point(20, 48), Width = 330, DropDownStyle = ComboBoxStyle.DropDownList }; combo.Items.AddRange(choices); combo.SelectedIndex = 0; Controls.Add(combo);
            Button ok = Theme.Button("Confirmar", delegate { DialogResult = DialogResult.OK; Close(); }, true); ok.Location = new Point(205, 92); Controls.Add(ok);
        }
    }

    public class ErpCredentialsForm : Form
    {
        private TextBox email;
        private TextBox password;
        public string Email { get { return email.Text.Trim(); } }
        public string Password { get { return password.Text; } }

        public ErpCredentialsForm(string savedEmail)
        {
            Text = "Conectar ao ERP Nativo"; Width = 510; Height = 340; StartPosition = FormStartPosition.CenterParent;
            BackColor = Theme.Paper; Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            Label title = Theme.Label("Primeira sincronização", 18, FontStyle.Bold, Theme.Brown); title.Location = new Point(28, 24); Controls.Add(title);
            Label help = Theme.Label("Use o mesmo e-mail e a mesma senha cadastrados no ERP online. A senha não será salva.", 9, FontStyle.Regular, Theme.Muted); help.Location = new Point(30, 65); help.MaximumSize = new Size(430, 0); Controls.Add(help);
            Label emailLabel = Theme.Label("E-mail do ERP", 8, FontStyle.Bold, Theme.Brown); emailLabel.Location = new Point(28, 112); Controls.Add(emailLabel);
            email = new TextBox { Location = new Point(30, 135), Width = 430, Text = savedEmail ?? "", Font = new Font("Segoe UI", 10) }; Controls.Add(email);
            Label passwordLabel = Theme.Label("Senha do ERP", 8, FontStyle.Bold, Theme.Brown); passwordLabel.Location = new Point(28, 176); Controls.Add(passwordLabel);
            password = new TextBox { Location = new Point(30, 199), Width = 430, UseSystemPasswordChar = true, Font = new Font("Segoe UI", 10) }; Controls.Add(password);
            Button cancel = Theme.Button("Cancelar", delegate { DialogResult = DialogResult.Cancel; Close(); }, false); cancel.Location = new Point(210, 248); cancel.Width = 110; Controls.Add(cancel);
            Button connect = Theme.Button("Conectar e sincronizar", delegate {
                if (String.IsNullOrWhiteSpace(email.Text) || !email.Text.Contains("@")) { MessageBox.Show("Informe o e-mail cadastrado no ERP.", "Nativo", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
                if (password.Text.Length < 6) { MessageBox.Show("Informe a senha do ERP com pelo menos 6 caracteres.", "Nativo", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
                DialogResult = DialogResult.OK; Close();
            }, true); connect.Location = new Point(330, 248); connect.Width = 130; Controls.Add(connect); AcceptButton = connect;
        }
    }

    public static class PromptForm
    {
        public static string Ask(string title, string initial)
        {
            using (Form form = new Form())
            {
                form.Text = title; form.Width = 420; form.Height = 170; form.StartPosition = FormStartPosition.CenterParent; form.FormBorderStyle = FormBorderStyle.FixedDialog; form.MaximizeBox = false; form.MinimizeBox = false;
                TextBox box = new TextBox { Left = 20, Top = 24, Width = 360, Text = initial }; form.Controls.Add(box);
                Button ok = Theme.Button("Confirmar", delegate { form.DialogResult = DialogResult.OK; form.Close(); }, true); ok.Left = 235; ok.Top = 66; form.Controls.Add(ok); form.AcceptButton = ok;
                return form.ShowDialog() == DialogResult.OK ? box.Text.Trim() : "";
            }
        }
    }

    public class LoginForm : Form
    {
        private TextBox userName;
        private TextBox password;
        private CheckBox rememberLogin;
        private CheckBox openCash;
        private AppState state;
        public string AuthenticatedRole { get; private set; }
        public string AuthenticatedName { get; private set; }
        public bool OpenCashAfterLogin { get { return openCash != null && openCash.Checked; } }

        public LoginForm(AppState appState)
        {
            state = appState;
            Text = "Entrar no Ambrosia"; Width = 470; Height = 485; StartPosition = FormStartPosition.CenterScreen; BackColor = Theme.Paper;
            Font = new Font("Segoe UI", 9); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false;
            try
            {
                string installedIcon = Path.Combine(StateStore.DataFolder, "NativoSistemas.ico");
                Icon = File.Exists(installedIcon) ? new Icon(installedIcon) : Icon.ExtractAssociatedIcon(Application.ExecutablePath);
            }
            catch { }
            Panel header = new Panel { Dock = DockStyle.Top, Height = 112, BackColor = Theme.Brown }; Controls.Add(header);
            PictureBox brand = new PictureBox { Location = new Point(182, 6), Size = new Size(100, 100), SizeMode = PictureBoxSizeMode.Zoom, Image = StateStore.LoadBrandImage(), BackColor = Theme.Brown }; header.Controls.Add(brand);
            Label title = Theme.Label("Acesso ao sistema", 17, FontStyle.Bold, Theme.Brown); title.Location = new Point(34, 138); Controls.Add(title);
            Label help = Theme.Label("Digite o usuário e a senha para entrar no sistema.", 9, FontStyle.Regular, Theme.Muted); help.Location = new Point(36, 174); Controls.Add(help);
            Label userLabel = Theme.Label("Usuário", 9, FontStyle.Bold, Theme.Brown); userLabel.Location = new Point(34, 211); Controls.Add(userLabel);
            userName = new TextBox { Location = new Point(36, 236), Width = 385, Font = new Font("Segoe UI", 10), Text = state.RememberLogin && !String.IsNullOrWhiteSpace(state.RememberedUsername) ? state.RememberedUsername : "ambrosia adm" }; Controls.Add(userName);
            Label passwordLabel = Theme.Label("Senha", 9, FontStyle.Bold, Theme.Brown); passwordLabel.Location = new Point(34, 273); Controls.Add(passwordLabel);
            password = new TextBox { Location = new Point(36, 298), Width = 385, Font = new Font("Segoe UI", 10), UseSystemPasswordChar = true }; Controls.Add(password);
            rememberLogin = new CheckBox { Text = "Salvar usuário neste computador", Location = new Point(36, 331), AutoSize = true, Checked = state.RememberLogin, ForeColor = Theme.Brown };
            Controls.Add(rememberLogin);
            openCash = new CheckBox { Text = "Abrir o Caixa depois de entrar", Location = new Point(36, 354), AutoSize = true, ForeColor = Theme.Brown };
            Controls.Add(openCash);
            Label safety = Theme.Label("A senha não é salva. O acesso ao ERP é protegido pelo perfil.", 8, FontStyle.Regular, Theme.Muted); safety.Location = new Point(36, 378); Controls.Add(safety);
            Button showPassword = Theme.Button("Mostrar senha", delegate { password.UseSystemPasswordChar = !password.UseSystemPasswordChar; }, false);
            showPassword.Location = new Point(36, 407); showPassword.Width = 150;
            showPassword.Click += delegate { showPassword.Text = password.UseSystemPasswordChar ? "Mostrar senha" : "Ocultar senha"; };
            Controls.Add(showPassword);
            Button enter = Theme.Button("Entrar", delegate { ValidateLogin(); }, true); enter.Location = new Point(276, 407); enter.Width = 145; Controls.Add(enter);
            AcceptButton = enter; password.Focus();
        }

        private void ValidateLogin()
        {
            string login = userName.Text.Trim();
            AppUser user = state.AppUsers.FirstOrDefault(x => x.Active && String.Equals(x.Username, login, StringComparison.OrdinalIgnoreCase));
            if (user == null && String.Equals(login, "ambrosia", StringComparison.OrdinalIgnoreCase))
                user = state.AppUsers.FirstOrDefault(x => x.Active && String.Equals(x.Username, "ambrosia adm", StringComparison.OrdinalIgnoreCase));
            if (user != null && StateStore.HashPassword(password.Text) == user.PasswordHash)
            {
                state.RememberLogin = rememberLogin.Checked;
                state.RememberedUsername = rememberLogin.Checked ? user.Username : "";
                StateStore.Save(state);
                AuthenticatedRole = String.Equals(user.Role, "admin", StringComparison.OrdinalIgnoreCase) ? "admin" : (String.Equals(user.Role, "caixa", StringComparison.OrdinalIgnoreCase) ? "caixa" : "atendimento");
                AuthenticatedName = String.IsNullOrWhiteSpace(user.DisplayName) ? user.Username : user.DisplayName;
                DialogResult = DialogResult.OK; Close(); return;
            }
            MessageBox.Show("Usuário ou senha incorretos.", "Acesso negado", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            password.Clear(); password.Focus();
        }

    }

    public static class Program
    {
        [STAThread]
        public static void Main()
        {
            Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false);
            AppState state = StateStore.Load();
            using (LoginForm login = new LoginForm(state))
            {
                if (login.ShowDialog() != DialogResult.OK) return;
                Application.Run(new MainForm(login.AuthenticatedRole, login.AuthenticatedName, login.OpenCashAfterLogin));
            }
        }
    }
}
