Spaces:
Sleeping
Sleeping
| 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<LocalReportDefinition> 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<string> headers, IEnumerable<IReadOnlyList<string>> 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<LocalReportDefinition> 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); | |
| } | |