using System.Text; using BakeryErp.Infrastructure.Database; namespace BakeryErp.Infrastructure.Repositories; public sealed class ReportCenterRepository { private readonly DatabaseOptions _options; public ReportCenterRepository(DatabaseOptions options) { _options = options; } public IReadOnlyList List() { return LocalDataStoreFile.Load(_options) .ReportDefinitions .OrderBy(static report => report.ReportCategory) .ThenBy(static report => report.ReportCode) .ToList(); } public LocalReportDefinition? Find(Guid id) { return LocalDataStoreFile.Load(_options) .ReportDefinitions .FirstOrDefault(report => report.Id == id); } public ReportCenterSummary GetSummary() { var store = LocalDataStoreFile.Load(_options); return new ReportCenterSummary( store.ReportDefinitions.Count, store.ReportDefinitions.Count(static report => report.Status == LocalReportStatus.PendingApproval), store.ReportDefinitions.Count(static report => report.Status == LocalReportStatus.Approved), store.StockBalances.Count, store.PurchaseOrders.Count, store.PettyCashTransactions.Count, store.TransferOrders.Count, store.StoreReturns.Count); } public SaveReportDefinitionResult Save(LocalReportDefinition report) { var store = LocalDataStoreFile.Load(_options); var reportCode = report.ReportCode.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(reportCode) || string.IsNullOrWhiteSpace(report.ReportName) || string.IsNullOrWhiteSpace(report.ReportCategory)) { return SaveReportDefinitionResult.ValidationError("報表代號、名稱與分類為必填。"); } var duplicatedCode = store.ReportDefinitions.Any(existing => existing.Id != report.Id && string.Equals(existing.ReportCode, reportCode, StringComparison.OrdinalIgnoreCase)); if (duplicatedCode) { return SaveReportDefinitionResult.ValidationError("報表代號已存在,請改用其他代號。"); } var existingReport = store.ReportDefinitions.FirstOrDefault(existing => existing.Id == report.Id); if (existingReport is null) { report.Id = report.Id == Guid.Empty ? Guid.NewGuid() : report.Id; report.ReportCode = reportCode; report.ReportName = report.ReportName.Trim(); report.ReportCategory = report.ReportCategory.Trim(); report.Description = BlankToNull(report.Description); report.Status = LocalReportStatus.PendingApproval; report.CreatedAt = DateTime.UtcNow; store.ReportDefinitions.Add(report); } else { if (existingReport.Status == LocalReportStatus.Approved) { return SaveReportDefinitionResult.ValidationError("已審核報表不可編輯。"); } existingReport.ReportCode = reportCode; existingReport.ReportName = report.ReportName.Trim(); existingReport.ReportCategory = report.ReportCategory.Trim(); existingReport.Description = BlankToNull(report.Description); existingReport.Status = LocalReportStatus.PendingApproval; existingReport.UpdatedAt = DateTime.UtcNow; } LocalDataStoreFile.Save(_options, store); return SaveReportDefinitionResult.Success(); } public void Approve(Guid id) { Update(id, report => { report.Status = LocalReportStatus.Approved; report.ApprovedAt = DateTime.UtcNow; }); } public void Cancel(Guid id) { Update(id, report => report.Status = LocalReportStatus.Cancelled); } public CsvExport Export(string reportCode) { var store = LocalDataStoreFile.Load(_options); return reportCode.ToUpperInvariant() switch { "RPT-INVENTORY" => ExportInventory(store), "RPT-PURCHASE" => ExportPurchases(store), "RPT-PETTYCASH" => ExportPettyCash(store), "RPT-TRANSFERS" => ExportTransfers(store), "RPT-RETURNS" => ExportReturns(store), _ => ExportReportDefinitions(store) }; } public static string GetStatusName(LocalReportStatus status) { return status switch { LocalReportStatus.Draft => "草稿", LocalReportStatus.PendingApproval => "待審核", LocalReportStatus.Approved => "已審核", LocalReportStatus.Cancelled => "已取消", _ => status.ToString() }; } private static CsvExport ExportInventory(LocalDataStore store) { var rows = store.StockBalances.Select(stock => new[] { stock.WarehouseName, stock.ItemSku, stock.ItemName, stock.QuantityOnHand.ToString("0.##"), stock.SafetyStockQuantity.ToString("0.##"), stock.Unit }); return Csv("inventory-report.csv", ["倉庫", "品項代號", "品項名稱", "現有庫存", "安全庫存", "單位"], rows); } private static CsvExport ExportPurchases(LocalDataStore store) { var rows = store.PurchaseOrders.Select(order => new[] { order.OrderNumber, order.SupplierName, order.OrderDate.ToString("yyyy-MM-dd"), order.ItemName, order.Quantity.ToString("0.##"), order.UnitPrice.ToString("0.##"), order.LineAmount.ToString("0.##"), PurchaseRepository.GetStatusName(order.Status) }); return Csv("purchase-report.csv", ["採購單號", "供應商", "日期", "品項", "數量", "單價", "金額", "狀態"], rows); } private static CsvExport ExportPettyCash(LocalDataStore store) { var rows = store.PettyCashTransactions.Select(transaction => new[] { transaction.TransactionNumber, transaction.StoreName, transaction.TransactionDate.ToString("yyyy-MM-dd"), PettyCashRepository.GetTransactionTypeName(transaction.TransactionType), transaction.ExpenseCategory, transaction.Amount.ToString("0.##"), PettyCashRepository.GetStatusName(transaction.Status) }); return Csv("petty-cash-report.csv", ["單號", "門市", "日期", "類型", "分類", "金額", "狀態"], rows); } private static CsvExport ExportTransfers(LocalDataStore store) { var rows = store.TransferOrders.Select(order => new[] { order.TransferNumber, order.StoreName, order.ItemName, order.RequestedQuantity.ToString("0.##"), order.ShippedQuantity.ToString("0.##"), order.ReceivedQuantity.ToString("0.##"), order.DifferenceQuantity.ToString("0.##"), TransferRepository.GetStatusName(order.Status) }); return Csv("transfer-report.csv", ["調撥單號", "門市", "品項", "叫貨", "出庫", "收貨", "差異", "狀態"], rows); } private static CsvExport ExportReturns(LocalDataStore store) { var rows = store.StoreReturns.Select(item => new[] { item.ReturnNumber, item.StoreName, item.ItemName, item.Quantity.ToString("0.##"), item.Reason, StoreReturnRepository.GetStatusName(item.Status), StoreReturnRepository.GetDispositionName(item.Disposition) }); return Csv("store-return-report.csv", ["退貨單號", "門市", "品項", "數量", "原因", "狀態", "處理方式"], rows); } private static CsvExport ExportReportDefinitions(LocalDataStore store) { var rows = store.ReportDefinitions.Select(report => new[] { report.ReportCode, report.ReportName, report.ReportCategory, ReportCenterRepository.GetStatusName(report.Status) }); return Csv("report-definitions.csv", ["報表代號", "報表名稱", "分類", "狀態"], rows); } private static CsvExport Csv(string fileName, IReadOnlyList headers, IEnumerable> rows) { var builder = new StringBuilder(); builder.AppendLine(string.Join(",", headers.Select(EscapeCsv))); foreach (var row in rows) { builder.AppendLine(string.Join(",", row.Select(EscapeCsv))); } return new CsvExport(fileName, "\uFEFF" + builder); } private static string EscapeCsv(string value) { var normalized = value.Replace("\"", "\"\"", StringComparison.Ordinal); return $"\"{normalized}\""; } private void Update(Guid id, Action update) { var store = LocalDataStoreFile.Load(_options); var report = store.ReportDefinitions.FirstOrDefault(existing => existing.Id == id); if (report is null) { return; } update(report); report.UpdatedAt = DateTime.UtcNow; LocalDataStoreFile.Save(_options, store); } private static string? BlankToNull(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } } public sealed record ReportCenterSummary( int ReportCount, int PendingApprovalCount, int ApprovedCount, int InventoryRows, int PurchaseRows, int PettyCashRows, int TransferRows, int ReturnRows); public sealed record CsvExport(string FileName, string Content); public sealed record SaveReportDefinitionResult(bool IsSuccess, string? ErrorMessage) { public static SaveReportDefinitionResult Success() => new(true, null); public static SaveReportDefinitionResult ValidationError(string message) => new(false, message); }