diff --git a/QuickBooksDesktop.slnx b/QuickBooksDesktop.slnx index 9686701..ae06a54 100644 --- a/QuickBooksDesktop.slnx +++ b/QuickBooksDesktop.slnx @@ -8,6 +8,7 @@ + diff --git a/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs b/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs index e9441be..0927165 100644 --- a/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs +++ b/src/Core/QBD.Application/DTOs/RegisterEntryDto.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Enums; namespace QBD.Application.ViewModels; diff --git a/src/Core/QBD.Application/DTOs/ReportColumnDto.cs b/src/Core/QBD.Application/DTOs/ReportColumnDto.cs index 736c52b..b4ad81d 100644 --- a/src/Core/QBD.Application/DTOs/ReportColumnDto.cs +++ b/src/Core/QBD.Application/DTOs/ReportColumnDto.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Application.ViewModels; public class ReportColumnDto diff --git a/src/Core/QBD.Application/DTOs/ReportRowDto.cs b/src/Core/QBD.Application/DTOs/ReportRowDto.cs index 7d80bd5..d53585f 100644 --- a/src/Core/QBD.Application/DTOs/ReportRowDto.cs +++ b/src/Core/QBD.Application/DTOs/ReportRowDto.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Application.ViewModels; public class ReportRowDto diff --git a/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs b/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs index 2a36e5c..08286ca 100644 --- a/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs +++ b/src/Core/QBD.Application/DTOs/TransactionSummaryDto.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Enums; namespace QBD.Application.ViewModels; diff --git a/src/Core/QBD.Application/Interfaces/IAuditService.cs b/src/Core/QBD.Application/Interfaces/IAuditService.cs index b5f58c1..8892212 100644 --- a/src/Core/QBD.Application/Interfaces/IAuditService.cs +++ b/src/Core/QBD.Application/Interfaces/IAuditService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Application.Interfaces; public interface IAuditService diff --git a/src/Core/QBD.Application/Interfaces/IFileDialogService.cs b/src/Core/QBD.Application/Interfaces/IFileDialogService.cs new file mode 100644 index 0000000..890dcec --- /dev/null +++ b/src/Core/QBD.Application/Interfaces/IFileDialogService.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +namespace QBD.Application.Interfaces +{ + public interface IFileDialogService + { + string? ShowSaveFileDialog(string fileName, string defaultExt, string filter); + } +} diff --git a/src/Core/QBD.Application/Interfaces/INavigationService.cs b/src/Core/QBD.Application/Interfaces/INavigationService.cs index 389ec9a..b3999f7 100644 --- a/src/Core/QBD.Application/Interfaces/INavigationService.cs +++ b/src/Core/QBD.Application/Interfaces/INavigationService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Application.Interfaces; public interface INavigationService diff --git a/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs b/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs index 169c6a1..3f92028 100644 --- a/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs +++ b/src/Core/QBD.Application/Interfaces/INumberSequenceService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Application.Interfaces; public interface INumberSequenceService diff --git a/src/Core/QBD.Application/Interfaces/IPdfExportService.cs b/src/Core/QBD.Application/Interfaces/IPdfExportService.cs new file mode 100644 index 0000000..a98dfa1 --- /dev/null +++ b/src/Core/QBD.Application/Interfaces/IPdfExportService.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using System.Threading.Tasks; +using QBD.Domain.Entities.Customers; + +namespace QBD.Application.Interfaces +{ + public interface IPdfExportService + { + Task ExportInvoiceToPdfAsync(Invoice invoice, string filePath); + + Task ExportReportToPdfAsync(string reportTitle, object reportData, string filePath); + } +} \ No newline at end of file diff --git a/src/Core/QBD.Application/Interfaces/IRepository.cs b/src/Core/QBD.Application/Interfaces/IRepository.cs index 25506a4..281c6c5 100644 --- a/src/Core/QBD.Application/Interfaces/IRepository.cs +++ b/src/Core/QBD.Application/Interfaces/IRepository.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Linq.Expressions; using QBD.Domain.Common; diff --git a/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs b/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs index 21c9140..571b715 100644 --- a/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs +++ b/src/Core/QBD.Application/Interfaces/ITransactionPostingService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Enums; namespace QBD.Application.Interfaces; diff --git a/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs b/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs index 08f5d44..6582487 100644 --- a/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs +++ b/src/Core/QBD.Application/Interfaces/IUnitOfWork.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Application.Interfaces; public interface IUnitOfWork : IDisposable diff --git a/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs b/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs index 6be9fe8..8fc529a 100644 --- a/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs +++ b/src/Core/QBD.Application/ViewModels/CenterViewModelBase.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs b/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs index c44051f..0421f23 100644 --- a/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs +++ b/src/Core/QBD.Application/ViewModels/HomePageViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using CommunityToolkit.Mvvm.Input; using QBD.Application.Interfaces; diff --git a/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs b/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs index aa31762..c195924 100644 --- a/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs +++ b/src/Core/QBD.Application/ViewModels/ListViewModelBase.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs b/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs index efa2777..b8e6e4b 100644 --- a/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs +++ b/src/Core/QBD.Application/ViewModels/RegisterViewModelBase.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs b/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs index 1809b04..b854e20 100644 --- a/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs +++ b/src/Core/QBD.Application/ViewModels/ReportViewModelBase.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs b/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs index fdb2a29..9c76efe 100644 --- a/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs +++ b/src/Core/QBD.Application/ViewModels/TransactionFormViewModelBase.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Core/QBD.Application/ViewModels/ViewModelBase.cs b/src/Core/QBD.Application/ViewModels/ViewModelBase.cs index 06bb58b..60a2eed 100644 --- a/src/Core/QBD.Application/ViewModels/ViewModelBase.cs +++ b/src/Core/QBD.Application/ViewModels/ViewModelBase.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Core/QBD.Domain/Common/AuditLogEntry.cs b/src/Core/QBD.Domain/Common/AuditLogEntry.cs index 7b549e0..eda8cd3 100644 --- a/src/Core/QBD.Domain/Common/AuditLogEntry.cs +++ b/src/Core/QBD.Domain/Common/AuditLogEntry.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Domain.Common; public class AuditLogEntry diff --git a/src/Core/QBD.Domain/Common/BaseEntity.cs b/src/Core/QBD.Domain/Common/BaseEntity.cs index 73ff742..67fa221 100644 --- a/src/Core/QBD.Domain/Common/BaseEntity.cs +++ b/src/Core/QBD.Domain/Common/BaseEntity.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.ComponentModel.DataAnnotations; namespace QBD.Domain.Common; diff --git a/src/Core/QBD.Domain/Common/NumberSequence.cs b/src/Core/QBD.Domain/Common/NumberSequence.cs index c86c079..4850dec 100644 --- a/src/Core/QBD.Domain/Common/NumberSequence.cs +++ b/src/Core/QBD.Domain/Common/NumberSequence.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Domain.Common; public class NumberSequence : BaseEntity diff --git a/src/Core/QBD.Domain/Entities/Accounting/Account.cs b/src/Core/QBD.Domain/Entities/Accounting/Account.cs index 702517f..c041577 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/Account.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/Account.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Accounting/Class.cs b/src/Core/QBD.Domain/Entities/Accounting/Class.cs index bb7976c..d46bd60 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/Class.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/Class.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs b/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs index a6956a4..736ddba 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/FiscalPeriod.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs b/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs index 8f432b7..0694623 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/FiscalYear.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs b/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs index ad26031..dfda5bd 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/GLEntry.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs b/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs index c230bc3..2a62f95 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/JournalEntry.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs b/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs index a075686..75304c6 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/JournalEntryLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/Location.cs b/src/Core/QBD.Domain/Entities/Accounting/Location.cs index f74566b..08eeec7 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/Location.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/Location.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs b/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs index decfdcd..055cb56 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/PaymentMethod.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs b/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs index 2afb440..75ba0ad 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/TaxCode.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Accounting/Terms.cs b/src/Core/QBD.Domain/Entities/Accounting/Terms.cs index 3b5c0c3..505d2dc 100644 --- a/src/Core/QBD.Domain/Entities/Accounting/Terms.cs +++ b/src/Core/QBD.Domain/Entities/Accounting/Terms.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Banking/Check.cs b/src/Core/QBD.Domain/Entities/Banking/Check.cs index 72bc7a4..bf5cc0d 100644 --- a/src/Core/QBD.Domain/Entities/Banking/Check.cs +++ b/src/Core/QBD.Domain/Entities/Banking/Check.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs b/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs index c43aff9..a7ee55f 100644 --- a/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs +++ b/src/Core/QBD.Domain/Entities/Banking/CheckExpenseLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Customers; diff --git a/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs b/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs index 00d0432..e070fd1 100644 --- a/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs +++ b/src/Core/QBD.Domain/Entities/Banking/CheckItemLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Entities/Banking/Deposit.cs b/src/Core/QBD.Domain/Entities/Banking/Deposit.cs index 175e946..899fee7 100644 --- a/src/Core/QBD.Domain/Entities/Banking/Deposit.cs +++ b/src/Core/QBD.Domain/Entities/Banking/Deposit.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs b/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs index f4b5b07..e4e9ecc 100644 --- a/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs +++ b/src/Core/QBD.Domain/Entities/Banking/DepositLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs b/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs index b6fdd7d..26ce143 100644 --- a/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs +++ b/src/Core/QBD.Domain/Entities/Banking/Reconciliation.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs b/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs index 9ce09b3..fce3271 100644 --- a/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs +++ b/src/Core/QBD.Domain/Entities/Banking/ReconciliationLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Banking/Transfer.cs b/src/Core/QBD.Domain/Entities/Banking/Transfer.cs index 795940f..6876608 100644 --- a/src/Core/QBD.Domain/Entities/Banking/Transfer.cs +++ b/src/Core/QBD.Domain/Entities/Banking/Transfer.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs b/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs index 8d50050..7413041 100644 --- a/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs +++ b/src/Core/QBD.Domain/Entities/Company/CompanyInfo.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Company; diff --git a/src/Core/QBD.Domain/Entities/Company/Preference.cs b/src/Core/QBD.Domain/Entities/Company/Preference.cs index 45ec70e..6bcade0 100644 --- a/src/Core/QBD.Domain/Entities/Company/Preference.cs +++ b/src/Core/QBD.Domain/Entities/Company/Preference.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Company; diff --git a/src/Core/QBD.Domain/Entities/Company/User.cs b/src/Core/QBD.Domain/Entities/Company/User.cs index 7ad76c7..2b64528 100644 --- a/src/Core/QBD.Domain/Entities/Company/User.cs +++ b/src/Core/QBD.Domain/Entities/Company/User.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Company; diff --git a/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs b/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs index 510b4e2..e047049 100644 --- a/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs +++ b/src/Core/QBD.Domain/Entities/Customers/CreditMemo.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs b/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs index 3fc1e0a..8951c7c 100644 --- a/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs +++ b/src/Core/QBD.Domain/Entities/Customers/CreditMemoLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Entities/Customers/Customer.cs b/src/Core/QBD.Domain/Entities/Customers/Customer.cs index bea1819..94e0e12 100644 --- a/src/Core/QBD.Domain/Entities/Customers/Customer.cs +++ b/src/Core/QBD.Domain/Entities/Customers/Customer.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Customers/Estimate.cs b/src/Core/QBD.Domain/Entities/Customers/Estimate.cs index 624fa96..b931097 100644 --- a/src/Core/QBD.Domain/Entities/Customers/Estimate.cs +++ b/src/Core/QBD.Domain/Entities/Customers/Estimate.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs b/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs index e7b1979..368f7fe 100644 --- a/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs +++ b/src/Core/QBD.Domain/Entities/Customers/EstimateLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Entities/Customers/Invoice.cs b/src/Core/QBD.Domain/Entities/Customers/Invoice.cs index b101dda..47ffb16 100644 --- a/src/Core/QBD.Domain/Entities/Customers/Invoice.cs +++ b/src/Core/QBD.Domain/Entities/Customers/Invoice.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs b/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs index 2724d85..2e9b6fa 100644 --- a/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs +++ b/src/Core/QBD.Domain/Entities/Customers/InvoiceLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Entities/Customers/Job.cs b/src/Core/QBD.Domain/Entities/Customers/Job.cs index 28cf311..8563304 100644 --- a/src/Core/QBD.Domain/Entities/Customers/Job.cs +++ b/src/Core/QBD.Domain/Entities/Customers/Job.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Customers; diff --git a/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs b/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs index 85094fa..7882615 100644 --- a/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs +++ b/src/Core/QBD.Domain/Entities/Customers/PaymentApplication.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; namespace QBD.Domain.Entities.Customers; diff --git a/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs b/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs index 447ec1e..e8f02ac 100644 --- a/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs +++ b/src/Core/QBD.Domain/Entities/Customers/ReceivePayment.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs b/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs index 36c82c1..725317d 100644 --- a/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs +++ b/src/Core/QBD.Domain/Entities/Customers/SalesReceipt.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs b/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs index 3fb1e20..db52a80 100644 --- a/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs +++ b/src/Core/QBD.Domain/Entities/Customers/SalesReceiptLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Entities/Items/Item.cs b/src/Core/QBD.Domain/Entities/Items/Item.cs index 1dbf460..2aa710a 100644 --- a/src/Core/QBD.Domain/Entities/Items/Item.cs +++ b/src/Core/QBD.Domain/Entities/Items/Item.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Vendors/Bill.cs b/src/Core/QBD.Domain/Entities/Vendors/Bill.cs index 0c637a2..8be5328 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/Bill.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/Bill.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs b/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs index 10c370b..55c60f6 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/BillExpenseLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Customers; diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs b/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs index 8e16d5a..e6d66e2 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/BillItemLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Customers; diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs b/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs index 1181ac5..e81c1d1 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/BillPayment.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs b/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs index 5e3c381..08cf797 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/BillPaymentApplication.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs index bed7979..02127d4 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrder.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs index 2394276..ff511b3 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/PurchaseOrderLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs b/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs index dc94334..d1c4146 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/Vendor.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs b/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs index 08818a7..a84f018 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/VendorCredit.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Enums; diff --git a/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs b/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs index a9b8236..58c3955 100644 --- a/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs +++ b/src/Core/QBD.Domain/Entities/Vendors/VendorCreditLine.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; using QBD.Domain.Entities.Items; diff --git a/src/Core/QBD.Domain/Enums/AccountType.cs b/src/Core/QBD.Domain/Enums/AccountType.cs index f8bbae1..3d3032c 100644 --- a/src/Core/QBD.Domain/Enums/AccountType.cs +++ b/src/Core/QBD.Domain/Enums/AccountType.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Domain.Enums; public enum AccountType diff --git a/src/Core/QBD.Domain/Enums/DocStatus.cs b/src/Core/QBD.Domain/Enums/DocStatus.cs index c3b500f..2dc6d2d 100644 --- a/src/Core/QBD.Domain/Enums/DocStatus.cs +++ b/src/Core/QBD.Domain/Enums/DocStatus.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Domain.Enums; public enum DocStatus diff --git a/src/Core/QBD.Domain/Enums/ItemType.cs b/src/Core/QBD.Domain/Enums/ItemType.cs index 7057692..05356d8 100644 --- a/src/Core/QBD.Domain/Enums/ItemType.cs +++ b/src/Core/QBD.Domain/Enums/ItemType.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Domain.Enums; public enum ItemType diff --git a/src/Core/QBD.Domain/Enums/TransactionType.cs b/src/Core/QBD.Domain/Enums/TransactionType.cs index af64a08..196fa32 100644 --- a/src/Core/QBD.Domain/Enums/TransactionType.cs +++ b/src/Core/QBD.Domain/Enums/TransactionType.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + namespace QBD.Domain.Enums; public enum TransactionType diff --git a/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs b/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs index b4d6d8e..471dc6d 100644 --- a/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs +++ b/src/Infrastructure/QBD.Infrastructure/Data/DatabaseSeeder.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs b/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs index ad9b67c..0092624 100644 --- a/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs +++ b/src/Infrastructure/QBD.Infrastructure/Data/QBDesktopDbContext.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Domain.Common; using QBD.Domain.Entities.Accounting; diff --git a/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj b/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj index 8bd0377..49011dd 100644 --- a/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj +++ b/src/Infrastructure/QBD.Infrastructure/QBD.Infrastructure.csproj @@ -13,6 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs b/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs index 4947c94..83f5a44 100644 --- a/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs +++ b/src/Infrastructure/QBD.Infrastructure/Repositories/Repository.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs b/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs index 4b27aa7..6e9348a 100644 --- a/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs +++ b/src/Infrastructure/QBD.Infrastructure/Repositories/UnitOfWork.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore.Storage; using QBD.Application.Interfaces; using QBD.Infrastructure.Data; diff --git a/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs b/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs index 9b663ab..284bf53 100644 --- a/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs +++ b/src/Infrastructure/QBD.Infrastructure/Services/AuditService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Application.Interfaces; using QBD.Domain.Common; using QBD.Infrastructure.Data; diff --git a/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs b/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs index 7646c79..9146c55 100644 --- a/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs +++ b/src/Infrastructure/QBD.Infrastructure/Services/NumberSequenceService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; using QBD.Domain.Common; diff --git a/src/Infrastructure/QBD.Infrastructure/Services/PdfExportService.cs b/src/Infrastructure/QBD.Infrastructure/Services/PdfExportService.cs new file mode 100644 index 0000000..a05c499 --- /dev/null +++ b/src/Infrastructure/QBD.Infrastructure/Services/PdfExportService.cs @@ -0,0 +1,137 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QBD.Infrastructure.Services +{ + public class PdfExportService : IPdfExportService + { + static PdfExportService() + { + QuestPDF.Settings.License = LicenseType.Community; + } + + public async Task ExportInvoiceToPdfAsync(Invoice invoice, string filePath) + { + ArgumentNullException.ThrowIfNull(invoice); + + await Task.Run(() => + { + Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial")); + + page.Header().Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text("INVOICE").FontSize(28).SemiBold().FontColor(Colors.Blue.Darken2); + column.Item().Text($"Date: {invoice.Date:d}").FontSize(12); + column.Item().Text($"Invoice #: {invoice.InvoiceNumber ?? "N/A"}").FontSize(12); + }); + + row.ConstantItem(100).Height(50).Placeholder(); + }); + + page.Content().PaddingVertical(1, Unit.Centimetre).Column(column => + { + column.Spacing(5); + column.Item().Text("Billed To:").SemiBold(); + + var customerName = invoice.Customer?.CustomerName ?? "No Customer Selected"; + var address = invoice.BillToAddress ?? "No Address Provided"; + column.Item().Text($"{customerName}\n{address}"); + + column.Item().PaddingTop(25).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(25); // # + columns.RelativeColumn(3); // Item + columns.RelativeColumn(); // Qty + columns.RelativeColumn(); // Rate + columns.RelativeColumn(); // Amount + }); + + table.Header(header => + { + header.Cell().BorderBottom(1).Text("#").SemiBold(); + header.Cell().BorderBottom(1).Text("Item").SemiBold(); + header.Cell().BorderBottom(1).AlignRight().Text("Qty").SemiBold(); + header.Cell().BorderBottom(1).AlignRight().Text("Rate").SemiBold(); + header.Cell().BorderBottom(1).AlignRight().Text("Amount").SemiBold(); + }); + + int index = 1; + foreach (var line in invoice.Lines ?? new List()) + { + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).Text(index++.ToString()); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).Text(line.Item?.ItemName ?? line.Description ?? "Item"); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).AlignRight().Text(line.Qty.ToString()); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).AlignRight().Text($"{line.Rate:C}"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).AlignRight().Text($"{line.Amount:C}"); + } + }); + + column.Item().PaddingTop(10).AlignRight().Text($"Total: {invoice.Total:C}").FontSize(16).SemiBold(); + }); + + page.Footer().AlignCenter().Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + x.Span(" of "); + x.TotalPages(); + }); + }); + }) + .GeneratePdf(filePath); + }); + } + + public async Task ExportReportToPdfAsync(string reportTitle, object reportData, string filePath) + { + await Task.Run(() => + { + Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4.Landscape()); + page.Margin(1.5f, Unit.Centimetre); + page.PageColor(Colors.White); + + page.Header().Text(reportTitle).SemiBold().FontSize(20).FontColor(Colors.Blue.Darken2); + page.Content().PaddingVertical(1, Unit.Centimetre).Text("Report data export is currently in development..."); + + page.Footer().AlignCenter().Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + x.Span(" / "); + x.TotalPages(); + }); + }); + }) + .GeneratePdf(filePath); + }); + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs b/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs index f321267..4d9f2b0 100644 --- a/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs +++ b/src/Infrastructure/QBD.Infrastructure/Services/TransactionPostingService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; using QBD.Domain.Entities.Accounting; diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs index 4d3925c..cf4f21b 100644 --- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/BankRegisterViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs index 8e64c60..7974715 100644 --- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/MakeDepositsFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs index 047d38c..0d9ac22 100644 --- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/ReconcileViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs index 6a4644b..1d43963 100644 --- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/TransferFundsFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs index f812b16..a75f543 100644 --- a/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Banking/ViewModels/WriteChecksFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs index cc69cbb..b0407ff 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ChartOfAccountsViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using CommunityToolkit.Mvvm.Input; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs index 285ede9..92bae99 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ClassListViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; using QBD.Application.ViewModels; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs index 7d85bec..f8f597b 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/CompanyInfoFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs index 05b4900..93978af 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/ItemListViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; using QBD.Application.ViewModels; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs index d55ead7..61fcd8b 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PaymentMethodListViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; using QBD.Application.ViewModels; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs index 8a17e04..7843d7e 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/PreferencesFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; diff --git a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs index f6df260..e67dc33 100644 --- a/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Company/ViewModels/TermsListViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; using QBD.Application.ViewModels; diff --git a/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj b/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj index c5be2c4..3fe1447 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj +++ b/src/Presentation/Modules/QBD.Modules.Customers/QBD.Modules.Customers.csproj @@ -16,4 +16,4 @@ - + \ No newline at end of file diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs index ac06954..43ab50b 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CreditMemoFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs index 34afe94..e373f32 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/CustomerCenterViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs index 24a0a60..da24c0d 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/EstimateFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs index a98c92f..e06980e 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/InvoiceFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; @@ -7,6 +10,7 @@ using QBD.Domain.Entities.Customers; using QBD.Domain.Entities.Items; using QBD.Domain.Enums; +using CommunityToolkit.Mvvm.Input; namespace QBD.Modules.Customers.ViewModels; @@ -17,6 +21,8 @@ public partial class InvoiceFormViewModel : TransactionFormViewModelBase _itemRepository; private readonly IRepository _termsRepository; private readonly INumberSequenceService _numberSequenceService; + private readonly IPdfExportService _pdfExportService; + private readonly IFileDialogService _fileDialogService; [ObservableProperty] private ObservableCollection _customers = new(); [ObservableProperty] private ObservableCollection _items = new(); @@ -32,13 +38,17 @@ public InvoiceFormViewModel( IRepository customerRepository, IRepository itemRepository, IRepository termsRepository, - INumberSequenceService numberSequenceService) : base(unitOfWork, postingService, navigationService) + INumberSequenceService numberSequenceService, + IPdfExportService pdfExportService, + IFileDialogService fileDialogService) : base(unitOfWork, postingService, navigationService) { _invoiceRepository = invoiceRepository; _customerRepository = customerRepository; _itemRepository = itemRepository; _termsRepository = termsRepository; _numberSequenceService = numberSequenceService; + _pdfExportService = pdfExportService; + _fileDialogService = fileDialogService; Title = "Create Invoice"; } @@ -62,6 +72,7 @@ partial void OnSelectedCustomerChanged(Customer? value) { if (value != null) { + Header.Customer = value; Header.CustomerId = value.Id; Header.BillToAddress = value.BillToAddress; if (value.TermsId.HasValue) @@ -140,4 +151,35 @@ protected override async Task VoidAsync() } catch (Exception ex) { SetError(ex.Message); } } -} + + [RelayCommand] + private async Task ExportToPdfAsync() + { + if (Header == null) return; + + var filePath = _fileDialogService.ShowSaveFileDialog( + $"Invoice_{Header.InvoiceNumber ?? "New"}.pdf", + ".pdf", + "PDF Documents (.pdf)|*.pdf"); + + if (filePath != null) + { + IsBusy = true; + try + { + Header.Lines = Lines.Where(l => l.Amount != 0).ToList(); + RecalculateTotals(); + await _pdfExportService.ExportInvoiceToPdfAsync(Header, filePath); + SetStatus($"Invoice exported to {filePath}"); + } + catch (Exception ex) + { + SetError($"Export failed: {ex.Message}"); + } + finally + { + IsBusy = false; + } + } + } +} \ No newline at end of file diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs index 9c99889..8f6211c 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/ReceivePaymentFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs index c817378..6f1e4d1 100644 --- a/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Customers/ViewModels/SalesReceiptFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs index be413fc..57ed0c9 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingDetailReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs index 40cd9ca..d24547a 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/APAgingSummaryReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs index 5d35f15..711a27b 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingDetailReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs index c36d1fd..cbc2689 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ARAgingSummaryReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs index 77028f7..c2a200b 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/BalanceSheetReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs index 3e11c4d..05d413b 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CashFlowsReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs index 5ff7424..8638686 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/CustomerBalanceReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs index c7a80a6..bc1497a 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/DepositDetailReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs index 052518a..32400a9 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/GeneralLedgerReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs index 226d0b3..22195f7 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/OpenInvoicesReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs index 09a52e7..65d4aa0 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ProfitLossReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs index e441682..ae0a076 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/ReportCenterViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Application.Interfaces; using QBD.Application.ViewModels; using CommunityToolkit.Mvvm.Input; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs index 4bb481d..3576854 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TransactionListReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs index 31a6846..d08aa0f 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/TrialBalanceReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs index ecd9d2f..6a2195b 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/UnpaidBillsReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs index f354fde..16cb715 100644 --- a/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Reports/ViewModels/VendorBalanceReportViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using QBD.Application.Interfaces; diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs index 6c13c1f..b56b7d8 100644 --- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/BillFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs index 5eaf1f3..444f891 100644 --- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PayBillsFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs index cf3ede2..911f870 100644 --- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/PurchaseOrderFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs index e8b7ffb..5125cd0 100644 --- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCenterViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs index b132771..8223f12 100644 --- a/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs +++ b/src/Presentation/Modules/QBD.Modules.Vendors/ViewModels/VendorCreditFormViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.EntityFrameworkCore; diff --git a/src/Presentation/QBD.API/Controllers/AccountsController.cs b/src/Presentation/QBD.API/Controllers/AccountsController.cs new file mode 100644 index 0000000..443364e --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/AccountsController.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Accounting; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AccountsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IRepository _glRepo; + private readonly IUnitOfWork _uow; + + public AccountsController(IRepository repo, IRepository glRepo, IUnitOfWork uow) + { + _repo = repo; + _glRepo = glRepo; + _uow = uow; + } + + [HttpGet] + public async Task GetAll([FromQuery] AccountType? type, [FromQuery] bool? activeOnly) + { + var query = _repo.Query(); + if (type.HasValue) query = query.Where(a => a.AccountType == type.Value); + if (activeOnly == true) query = query.Where(a => a.IsActive); + var accounts = await query.OrderBy(a => a.SortOrder).ToListAsync(); + return Ok(accounts); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var account = await _repo.Query() + .Include(a => a.SubAccounts) + .FirstOrDefaultAsync(a => a.Id == id); + if (account == null) return NotFound(); + return Ok(account); + } + + [HttpPost] + public async Task Create([FromBody] Account account) + { + account.IsActive = true; + var created = await _repo.AddAsync(account); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Account account) + { + var existing = await _repo.GetByIdAsync(id); + if (existing == null) return NotFound(); + + existing.Name = account.Name; + existing.Number = account.Number; + existing.AccountType = account.AccountType; + existing.Description = account.Description; + existing.IsActive = account.IsActive; + existing.ParentId = account.ParentId; + + await _repo.UpdateAsync(existing); + await _uow.SaveChangesAsync(); + return Ok(existing); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var account = await _repo.GetByIdAsync(id); + if (account == null) return NotFound(); + if (account.IsSystemAccount) return BadRequest("Cannot delete system accounts."); + + await _repo.DeleteAsync(account); + await _uow.SaveChangesAsync(); + return NoContent(); + } + + [HttpGet("{id}/ledger")] + public async Task GetLedger(int id, [FromQuery] DateTime? from, [FromQuery] DateTime? to) + { + var account = await _repo.GetByIdAsync(id); + if (account == null) return NotFound(); + + var query = _glRepo.Query().Where(g => g.AccountId == id && !g.IsVoid); + if (from.HasValue) query = query.Where(g => g.PostingDate >= from.Value); + if (to.HasValue) query = query.Where(g => g.PostingDate <= to.Value); + + var entries = await query.OrderBy(g => g.PostingDate).ThenBy(g => g.Id).ToListAsync(); + return Ok(entries); + } +} diff --git a/src/Presentation/QBD.API/Controllers/AuditController.cs b/src/Presentation/QBD.API/Controllers/AuditController.cs new file mode 100644 index 0000000..84c901d --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/AuditController.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Infrastructure.Data; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuditController : ControllerBase +{ + private readonly QBDesktopDbContext _context; + + public AuditController(QBDesktopDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task GetAll( + [FromQuery] string? entityType, + [FromQuery] int? entityId, + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 50) + { + var query = _context.AuditLogEntries.AsQueryable(); + if (!string.IsNullOrEmpty(entityType)) query = query.Where(a => a.EntityType == entityType); + if (entityId.HasValue) query = query.Where(a => a.EntityId == entityId.Value); + if (from.HasValue) query = query.Where(a => a.Timestamp >= from.Value); + if (to.HasValue) query = query.Where(a => a.Timestamp <= to.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(a => a.Timestamp) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } +} diff --git a/src/Presentation/QBD.API/Controllers/BillPaymentsController.cs b/src/Presentation/QBD.API/Controllers/BillPaymentsController.cs new file mode 100644 index 0000000..f6cd3cb --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/BillPaymentsController.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Vendors; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BillPaymentsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly ITransactionPostingService _posting; + + public BillPaymentsController( + IRepository repo, + IUnitOfWork uow, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query() + .Include(bp => bp.PaymentAccount); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(bp => bp.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var payment = await _repo.Query() + .Include(bp => bp.PaymentAccount) + .Include(bp => bp.PaymentMethod) + .Include(bp => bp.Applications).ThenInclude(a => a.Bill).ThenInclude(b => b.Vendor) + .FirstOrDefaultAsync(bp => bp.Id == id); + if (payment == null) return NotFound(); + return Ok(payment); + } + + [HttpPost] + public async Task Create([FromBody] BillPayment payment) + { + payment.Status = DocStatus.Draft; + payment.Amount = payment.Applications.Sum(a => a.AmountApplied); + + var created = await _repo.AddAsync(payment); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var payment = await _repo.GetByIdAsync(id); + if (payment == null) return NotFound(); + if (payment.Status != DocStatus.Draft) return BadRequest("Only draft bill payments can be posted."); + + await _posting.PostTransactionAsync(TransactionType.BillPayment, id); + payment.Status = DocStatus.Posted; + await _repo.UpdateAsync(payment); + await _uow.SaveChangesAsync(); + return Ok(payment); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var payment = await _repo.GetByIdAsync(id); + if (payment == null) return NotFound(); + if (payment.Status == DocStatus.Posted) return BadRequest("Cannot delete posted bill payments."); + + await _repo.DeleteAsync(payment); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/BillsController.cs b/src/Presentation/QBD.API/Controllers/BillsController.cs new file mode 100644 index 0000000..76cb4bd --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/BillsController.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Vendors; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BillsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public BillsController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? vendorId, [FromQuery] DocStatus? status, + [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(b => b.Vendor).AsQueryable(); + if (vendorId.HasValue) query = query.Where(b => b.VendorId == vendorId.Value); + if (status.HasValue) query = query.Where(b => b.Status == status.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(b => b.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var bill = await _repo.Query() + .Include(b => b.Vendor) + .Include(b => b.ExpenseLines).ThenInclude(l => l.Account) + .Include(b => b.ItemLines).ThenInclude(l => l.Item) + .Include(b => b.Terms) + .FirstOrDefaultAsync(b => b.Id == id); + if (bill == null) return NotFound(); + return Ok(bill); + } + + [HttpPost] + public async Task Create([FromBody] Bill bill) + { + bill.BillNumber = await _numberSeq.GetNextNumberAsync("Bill"); + bill.Status = DocStatus.Draft; + bill.AmountDue = bill.ExpenseLines.Sum(l => l.Amount) + bill.ItemLines.Sum(l => l.Amount); + bill.BalanceDue = bill.AmountDue; + + var created = await _repo.AddAsync(bill); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var bill = await _repo.GetByIdAsync(id); + if (bill == null) return NotFound(); + if (bill.Status != DocStatus.Draft) return BadRequest("Only draft bills can be posted."); + + await _posting.PostTransactionAsync(TransactionType.Bill, id); + bill.Status = DocStatus.Posted; + await _repo.UpdateAsync(bill); + await _uow.SaveChangesAsync(); + return Ok(bill); + } + + [HttpPost("{id}/void")] + public async Task Void(int id) + { + var bill = await _repo.GetByIdAsync(id); + if (bill == null) return NotFound(); + if (bill.Status != DocStatus.Posted) return BadRequest("Only posted bills can be voided."); + + await _posting.VoidTransactionAsync(TransactionType.Bill, id); + bill.Status = DocStatus.Voided; + await _repo.UpdateAsync(bill); + await _uow.SaveChangesAsync(); + return Ok(bill); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var bill = await _repo.GetByIdAsync(id); + if (bill == null) return NotFound(); + if (bill.Status == DocStatus.Posted) return BadRequest("Cannot delete posted bills. Void first."); + + await _repo.DeleteAsync(bill); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/ChecksController.cs b/src/Presentation/QBD.API/Controllers/ChecksController.cs new file mode 100644 index 0000000..3317027 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/ChecksController.cs @@ -0,0 +1,108 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Banking; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ChecksController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public ChecksController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? bankAccountId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(c => c.BankAccount).AsQueryable(); + if (bankAccountId.HasValue) query = query.Where(c => c.BankAccountId == bankAccountId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(c => c.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var check = await _repo.Query() + .Include(c => c.BankAccount) + .Include(c => c.ExpenseLines).ThenInclude(l => l.Account) + .Include(c => c.ItemLines).ThenInclude(l => l.Item) + .FirstOrDefaultAsync(c => c.Id == id); + if (check == null) return NotFound(); + return Ok(check); + } + + [HttpPost] + public async Task Create([FromBody] Check check) + { + check.CheckNumber = await _numberSeq.GetNextNumberAsync("Check"); + check.Status = DocStatus.Draft; + check.Amount = check.ExpenseLines.Sum(l => l.Amount) + check.ItemLines.Sum(l => l.Amount); + + var created = await _repo.AddAsync(check); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var check = await _repo.GetByIdAsync(id); + if (check == null) return NotFound(); + if (check.Status != DocStatus.Draft) return BadRequest("Only draft checks can be posted."); + + await _posting.PostTransactionAsync(TransactionType.Check, id); + check.Status = DocStatus.Posted; + await _repo.UpdateAsync(check); + await _uow.SaveChangesAsync(); + return Ok(check); + } + + [HttpPost("{id}/void")] + public async Task Void(int id) + { + var check = await _repo.GetByIdAsync(id); + if (check == null) return NotFound(); + if (check.Status != DocStatus.Posted) return BadRequest("Only posted checks can be voided."); + + await _posting.VoidTransactionAsync(TransactionType.Check, id); + check.Status = DocStatus.Voided; + await _repo.UpdateAsync(check); + await _uow.SaveChangesAsync(); + return Ok(check); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var check = await _repo.GetByIdAsync(id); + if (check == null) return NotFound(); + if (check.Status == DocStatus.Posted) return BadRequest("Cannot delete posted checks."); + + await _repo.DeleteAsync(check); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/CompanyController.cs b/src/Presentation/QBD.API/Controllers/CompanyController.cs new file mode 100644 index 0000000..6f10721 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/CompanyController.cs @@ -0,0 +1,169 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Accounting; +using QBD.Domain.Entities.Company; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CompanyController : ControllerBase +{ + private readonly IRepository _companyRepo; + private readonly IRepository _termsRepo; + private readonly IRepository _paymentMethodRepo; + private readonly IRepository _taxCodeRepo; + private readonly IRepository _classRepo; + private readonly IRepository _locationRepo; + private readonly IUnitOfWork _uow; + + public CompanyController( + IRepository companyRepo, + IRepository termsRepo, + IRepository paymentMethodRepo, + IRepository taxCodeRepo, + IRepository classRepo, + IRepository locationRepo, + IUnitOfWork uow) + { + _companyRepo = companyRepo; + _termsRepo = termsRepo; + _paymentMethodRepo = paymentMethodRepo; + _taxCodeRepo = taxCodeRepo; + _classRepo = classRepo; + _locationRepo = locationRepo; + _uow = uow; + } + + // === Company Info === + + [HttpGet("info")] + public async Task GetCompanyInfo() + { + var info = await _companyRepo.Query().FirstOrDefaultAsync(); + if (info == null) return NotFound(); + return Ok(info); + } + + [HttpPut("info")] + public async Task UpdateCompanyInfo([FromBody] CompanyInfo info) + { + var existing = await _companyRepo.Query().FirstOrDefaultAsync(); + if (existing == null) return NotFound(); + + existing.Name = info.Name; + existing.LegalName = info.LegalName; + existing.Address = info.Address; + existing.City = info.City; + existing.State = info.State; + existing.Zip = info.Zip; + existing.Phone = info.Phone; + existing.Email = info.Email; + existing.EIN = info.EIN; + existing.FiscalYearStartMonth = info.FiscalYearStartMonth; + + await _companyRepo.UpdateAsync(existing); + await _uow.SaveChangesAsync(); + return Ok(existing); + } + + // === Terms === + + [HttpGet("terms")] + public async Task GetTerms() + { + var terms = await _termsRepo.GetAllAsync(); + return Ok(terms); + } + + [HttpPost("terms")] + public async Task CreateTerms([FromBody] Terms terms) + { + terms.IsActive = true; + var created = await _termsRepo.AddAsync(terms); + await _uow.SaveChangesAsync(); + return Ok(created); + } + + // === Payment Methods === + + [HttpGet("payment-methods")] + public async Task GetPaymentMethods() + { + var methods = await _paymentMethodRepo.GetAllAsync(); + return Ok(methods); + } + + [HttpPost("payment-methods")] + public async Task CreatePaymentMethod([FromBody] PaymentMethod method) + { + method.IsActive = true; + var created = await _paymentMethodRepo.AddAsync(method); + await _uow.SaveChangesAsync(); + return Ok(created); + } + + // === Tax Codes === + + [HttpGet("tax-codes")] + public async Task GetTaxCodes() + { + var codes = await _taxCodeRepo.GetAllAsync(); + return Ok(codes); + } + + [HttpPost("tax-codes")] + public async Task CreateTaxCode([FromBody] TaxCode code) + { + code.IsActive = true; + var created = await _taxCodeRepo.AddAsync(code); + await _uow.SaveChangesAsync(); + return Ok(created); + } + + // === Classes === + + [HttpGet("classes")] + public async Task GetClasses() + { + var classes = await _classRepo.Query() + .Include(c => c.SubClasses) + .Where(c => c.ParentId == null) + .ToListAsync(); + return Ok(classes); + } + + [HttpPost("classes")] + public async Task CreateClass([FromBody] Class cls) + { + cls.IsActive = true; + var created = await _classRepo.AddAsync(cls); + await _uow.SaveChangesAsync(); + return Ok(created); + } + + // === Locations === + + [HttpGet("locations")] + public async Task GetLocations() + { + var locations = await _locationRepo.Query() + .Include(l => l.SubLocations) + .Where(l => l.ParentId == null) + .ToListAsync(); + return Ok(locations); + } + + [HttpPost("locations")] + public async Task CreateLocation([FromBody] Location location) + { + location.IsActive = true; + var created = await _locationRepo.AddAsync(location); + await _uow.SaveChangesAsync(); + return Ok(created); + } +} diff --git a/src/Presentation/QBD.API/Controllers/CreditMemosController.cs b/src/Presentation/QBD.API/Controllers/CreditMemosController.cs new file mode 100644 index 0000000..79c407a --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/CreditMemosController.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CreditMemosController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public CreditMemosController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? customerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(c => c.Customer).AsQueryable(); + if (customerId.HasValue) query = query.Where(c => c.CustomerId == customerId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(c => c.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var memo = await _repo.Query() + .Include(c => c.Customer) + .Include(c => c.Lines).ThenInclude(l => l.Item) + .FirstOrDefaultAsync(c => c.Id == id); + if (memo == null) return NotFound(); + return Ok(memo); + } + + [HttpPost] + public async Task Create([FromBody] CreditMemo memo) + { + memo.CreditNumber = await _numberSeq.GetNextNumberAsync("CreditMemo"); + memo.Status = DocStatus.Draft; + memo.Subtotal = memo.Lines.Sum(l => l.Amount); + memo.Total = memo.Subtotal + memo.Tax; + memo.BalanceRemaining = memo.Total; + + var created = await _repo.AddAsync(memo); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var memo = await _repo.GetByIdAsync(id); + if (memo == null) return NotFound(); + if (memo.Status != DocStatus.Draft) return BadRequest("Only draft credit memos can be posted."); + + await _posting.PostTransactionAsync(TransactionType.CreditMemo, id); + memo.Status = DocStatus.Posted; + await _repo.UpdateAsync(memo); + await _uow.SaveChangesAsync(); + return Ok(memo); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var memo = await _repo.GetByIdAsync(id); + if (memo == null) return NotFound(); + if (memo.Status == DocStatus.Posted) return BadRequest("Cannot delete posted credit memos."); + + await _repo.DeleteAsync(memo); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/CustomersController.cs b/src/Presentation/QBD.API/Controllers/CustomersController.cs new file mode 100644 index 0000000..b4df80c --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/CustomersController.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CustomersController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + + public CustomersController(IRepository repo, IUnitOfWork uow) + { + _repo = repo; + _uow = uow; + } + + [HttpGet] + public async Task GetAll([FromQuery] bool? activeOnly, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var (items, total) = await _repo.GetPagedAsync(page, pageSize, + filter: activeOnly == true ? c => c.IsActive : null, + orderBy: c => c.CustomerName); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var customer = await _repo.Query() + .Include(c => c.Jobs) + .Include(c => c.Invoices) + .Include(c => c.Terms) + .FirstOrDefaultAsync(c => c.Id == id); + if (customer == null) return NotFound(); + return Ok(customer); + } + + [HttpPost] + public async Task Create([FromBody] Customer customer) + { + customer.IsActive = true; + var created = await _repo.AddAsync(customer); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Customer customer) + { + var existing = await _repo.GetByIdAsync(id); + if (existing == null) return NotFound(); + + existing.CustomerName = customer.CustomerName; + existing.Company = customer.Company; + existing.BillToAddress = customer.BillToAddress; + existing.ShipToAddress = customer.ShipToAddress; + existing.Phone = customer.Phone; + existing.Email = customer.Email; + existing.TermsId = customer.TermsId; + existing.CreditLimit = customer.CreditLimit; + existing.TaxCodeId = customer.TaxCodeId; + existing.IsActive = customer.IsActive; + + await _repo.UpdateAsync(existing); + await _uow.SaveChangesAsync(); + return Ok(existing); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var customer = await _repo.GetByIdAsync(id); + if (customer == null) return NotFound(); + + await _repo.DeleteAsync(customer); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/DepositsController.cs b/src/Presentation/QBD.API/Controllers/DepositsController.cs new file mode 100644 index 0000000..bf23a2e --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/DepositsController.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Banking; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class DepositsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly ITransactionPostingService _posting; + + public DepositsController( + IRepository repo, + IUnitOfWork uow, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? bankAccountId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(d => d.BankAccount).AsQueryable(); + if (bankAccountId.HasValue) query = query.Where(d => d.BankAccountId == bankAccountId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(d => d.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var deposit = await _repo.Query() + .Include(d => d.BankAccount) + .Include(d => d.Lines).ThenInclude(l => l.FromAccount) + .Include(d => d.Lines).ThenInclude(l => l.PaymentMethod) + .FirstOrDefaultAsync(d => d.Id == id); + if (deposit == null) return NotFound(); + return Ok(deposit); + } + + [HttpPost] + public async Task Create([FromBody] Deposit deposit) + { + deposit.Status = DocStatus.Draft; + deposit.Total = deposit.Lines.Sum(l => l.Amount); + + var created = await _repo.AddAsync(deposit); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var deposit = await _repo.GetByIdAsync(id); + if (deposit == null) return NotFound(); + if (deposit.Status != DocStatus.Draft) return BadRequest("Only draft deposits can be posted."); + + await _posting.PostTransactionAsync(TransactionType.Deposit, id); + deposit.Status = DocStatus.Posted; + await _repo.UpdateAsync(deposit); + await _uow.SaveChangesAsync(); + return Ok(deposit); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var deposit = await _repo.GetByIdAsync(id); + if (deposit == null) return NotFound(); + if (deposit.Status == DocStatus.Posted) return BadRequest("Cannot delete posted deposits."); + + await _repo.DeleteAsync(deposit); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/EstimatesController.cs b/src/Presentation/QBD.API/Controllers/EstimatesController.cs new file mode 100644 index 0000000..9ebfd74 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/EstimatesController.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class EstimatesController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + + public EstimatesController(IRepository repo, IUnitOfWork uow, INumberSequenceService numberSeq) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? customerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(e => e.Customer).AsQueryable(); + if (customerId.HasValue) query = query.Where(e => e.CustomerId == customerId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(e => e.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var estimate = await _repo.Query() + .Include(e => e.Customer) + .Include(e => e.Lines).ThenInclude(l => l.Item) + .FirstOrDefaultAsync(e => e.Id == id); + if (estimate == null) return NotFound(); + return Ok(estimate); + } + + [HttpPost] + public async Task Create([FromBody] Estimate estimate) + { + estimate.EstimateNumber = await _numberSeq.GetNextNumberAsync("Estimate"); + estimate.Status = DocStatus.Draft; + estimate.Subtotal = estimate.Lines.Sum(l => l.Amount); + estimate.Total = estimate.Subtotal + estimate.Tax; + + var created = await _repo.AddAsync(estimate); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var estimate = await _repo.GetByIdAsync(id); + if (estimate == null) return NotFound(); + + await _repo.DeleteAsync(estimate); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/InvoicesController.cs b/src/Presentation/QBD.API/Controllers/InvoicesController.cs new file mode 100644 index 0000000..10880b6 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/InvoicesController.cs @@ -0,0 +1,157 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class InvoicesController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public InvoicesController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? customerId, [FromQuery] DocStatus? status, + [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(i => i.Customer).AsQueryable(); + if (customerId.HasValue) query = query.Where(i => i.CustomerId == customerId.Value); + if (status.HasValue) query = query.Where(i => i.Status == status.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(i => i.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var invoice = await _repo.Query() + .Include(i => i.Customer) + .Include(i => i.Lines).ThenInclude(l => l.Item) + .Include(i => i.Terms) + .FirstOrDefaultAsync(i => i.Id == id); + if (invoice == null) return NotFound(); + return Ok(invoice); + } + + [HttpPost] + public async Task Create([FromBody] Invoice invoice) + { + invoice.InvoiceNumber = await _numberSeq.GetNextNumberAsync("Invoice"); + invoice.Status = DocStatus.Draft; + + // Calculate totals + invoice.Subtotal = invoice.Lines.Sum(l => l.Amount); + invoice.Total = invoice.Subtotal + invoice.TaxTotal; + invoice.BalanceDue = invoice.Total; + + var created = await _repo.AddAsync(invoice); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Invoice invoice) + { + var existing = await _repo.Query() + .Include(i => i.Lines) + .FirstOrDefaultAsync(i => i.Id == id); + if (existing == null) return NotFound(); + if (existing.Status != DocStatus.Draft) return BadRequest("Only draft invoices can be edited."); + + existing.CustomerId = invoice.CustomerId; + existing.Date = invoice.Date; + existing.DueDate = invoice.DueDate; + existing.TermsId = invoice.TermsId; + existing.Memo = invoice.Memo; + existing.BillToAddress = invoice.BillToAddress; + existing.ShipToAddress = invoice.ShipToAddress; + + // Replace lines + existing.Lines.Clear(); + foreach (var line in invoice.Lines) + { + existing.Lines.Add(new InvoiceLine + { + ItemId = line.ItemId, + Description = line.Description, + Qty = line.Qty, + Rate = line.Rate, + Amount = line.Amount, + TaxCodeId = line.TaxCodeId, + ClassId = line.ClassId + }); + } + + existing.Subtotal = existing.Lines.Sum(l => l.Amount); + existing.TaxTotal = invoice.TaxTotal; + existing.Total = existing.Subtotal + existing.TaxTotal; + existing.BalanceDue = existing.Total - existing.AmountPaid; + + await _repo.UpdateAsync(existing); + await _uow.SaveChangesAsync(); + return Ok(existing); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var invoice = await _repo.GetByIdAsync(id); + if (invoice == null) return NotFound(); + if (invoice.Status != DocStatus.Draft) return BadRequest("Only draft invoices can be posted."); + + await _posting.PostTransactionAsync(TransactionType.Invoice, id); + invoice.Status = DocStatus.Posted; + await _repo.UpdateAsync(invoice); + await _uow.SaveChangesAsync(); + return Ok(invoice); + } + + [HttpPost("{id}/void")] + public async Task Void(int id) + { + var invoice = await _repo.GetByIdAsync(id); + if (invoice == null) return NotFound(); + if (invoice.Status != DocStatus.Posted) return BadRequest("Only posted invoices can be voided."); + + await _posting.VoidTransactionAsync(TransactionType.Invoice, id); + invoice.Status = DocStatus.Voided; + await _repo.UpdateAsync(invoice); + await _uow.SaveChangesAsync(); + return Ok(invoice); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var invoice = await _repo.GetByIdAsync(id); + if (invoice == null) return NotFound(); + if (invoice.Status == DocStatus.Posted) return BadRequest("Cannot delete posted invoices. Void first."); + + await _repo.DeleteAsync(invoice); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/ItemsController.cs b/src/Presentation/QBD.API/Controllers/ItemsController.cs new file mode 100644 index 0000000..0a1ede4 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/ItemsController.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Items; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ItemsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + + public ItemsController(IRepository repo, IUnitOfWork uow) + { + _repo = repo; + _uow = uow; + } + + [HttpGet] + public async Task GetAll([FromQuery] ItemType? type, [FromQuery] bool? activeOnly) + { + var query = _repo.Query(); + if (type.HasValue) query = query.Where(i => i.ItemType == type.Value); + if (activeOnly == true) query = query.Where(i => i.IsActive); + var items = await query.OrderBy(i => i.ItemName).ToListAsync(); + return Ok(items); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var item = await _repo.Query() + .Include(i => i.IncomeAccount) + .Include(i => i.ExpenseAccount) + .Include(i => i.AssetAccount) + .Include(i => i.SubItems) + .FirstOrDefaultAsync(i => i.Id == id); + if (item == null) return NotFound(); + return Ok(item); + } + + [HttpPost] + public async Task Create([FromBody] Item item) + { + item.IsActive = true; + var created = await _repo.AddAsync(item); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Item item) + { + var existing = await _repo.GetByIdAsync(id); + if (existing == null) return NotFound(); + + existing.ItemName = item.ItemName; + existing.ItemType = item.ItemType; + existing.Description = item.Description; + existing.SalesPrice = item.SalesPrice; + existing.PurchaseCost = item.PurchaseCost; + existing.IncomeAccountId = item.IncomeAccountId; + existing.ExpenseAccountId = item.ExpenseAccountId; + existing.AssetAccountId = item.AssetAccountId; + existing.ReorderPoint = item.ReorderPoint; + existing.IsActive = item.IsActive; + existing.TaxCodeId = item.TaxCodeId; + + await _repo.UpdateAsync(existing); + await _uow.SaveChangesAsync(); + return Ok(existing); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var item = await _repo.GetByIdAsync(id); + if (item == null) return NotFound(); + + await _repo.DeleteAsync(item); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/JournalEntriesController.cs b/src/Presentation/QBD.API/Controllers/JournalEntriesController.cs new file mode 100644 index 0000000..c29f14d --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/JournalEntriesController.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Accounting; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class JournalEntriesController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public JournalEntriesController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var total = await _repo.Query().CountAsync(); + var items = await _repo.Query() + .OrderByDescending(j => j.PostingDate) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var entry = await _repo.Query() + .Include(j => j.Lines).ThenInclude(l => l.Account) + .FirstOrDefaultAsync(j => j.Id == id); + if (entry == null) return NotFound(); + return Ok(entry); + } + + [HttpPost] + public async Task Create([FromBody] JournalEntry entry) + { + // Validate balanced + var totalDebits = entry.Lines.Sum(l => l.DebitAmount); + var totalCredits = entry.Lines.Sum(l => l.CreditAmount); + if (totalDebits != totalCredits) + return BadRequest($"Debits ({totalDebits:C}) must equal Credits ({totalCredits:C})."); + + entry.EntryNumber = await _numberSeq.GetNextNumberAsync("JournalEntry"); + entry.Status = DocStatus.Draft; + + var created = await _repo.AddAsync(entry); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var entry = await _repo.GetByIdAsync(id); + if (entry == null) return NotFound(); + if (entry.Status != DocStatus.Draft) return BadRequest("Only draft entries can be posted."); + + await _posting.PostTransactionAsync(TransactionType.JournalEntry, id); + entry.Status = DocStatus.Posted; + await _repo.UpdateAsync(entry); + await _uow.SaveChangesAsync(); + return Ok(entry); + } + + [HttpPost("{id}/void")] + public async Task Void(int id) + { + var entry = await _repo.GetByIdAsync(id); + if (entry == null) return NotFound(); + if (entry.Status != DocStatus.Posted) return BadRequest("Only posted entries can be voided."); + + await _posting.VoidTransactionAsync(TransactionType.JournalEntry, id); + entry.Status = DocStatus.Voided; + await _repo.UpdateAsync(entry); + await _uow.SaveChangesAsync(); + return Ok(entry); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var entry = await _repo.GetByIdAsync(id); + if (entry == null) return NotFound(); + if (entry.Status == DocStatus.Posted) return BadRequest("Cannot delete posted journal entries."); + + await _repo.DeleteAsync(entry); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/PurchaseOrdersController.cs b/src/Presentation/QBD.API/Controllers/PurchaseOrdersController.cs new file mode 100644 index 0000000..1766830 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/PurchaseOrdersController.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Vendors; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class PurchaseOrdersController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + + public PurchaseOrdersController(IRepository repo, IUnitOfWork uow, INumberSequenceService numberSeq) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? vendorId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(p => p.Vendor).AsQueryable(); + if (vendorId.HasValue) query = query.Where(p => p.VendorId == vendorId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(p => p.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var po = await _repo.Query() + .Include(p => p.Vendor) + .Include(p => p.Lines).ThenInclude(l => l.Item) + .FirstOrDefaultAsync(p => p.Id == id); + if (po == null) return NotFound(); + return Ok(po); + } + + [HttpPost] + public async Task Create([FromBody] PurchaseOrder po) + { + po.PONumber = await _numberSeq.GetNextNumberAsync("PurchaseOrder"); + po.Status = DocStatus.Draft; + po.Subtotal = po.Lines.Sum(l => l.Amount); + po.Total = po.Subtotal; + + var created = await _repo.AddAsync(po); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var po = await _repo.GetByIdAsync(id); + if (po == null) return NotFound(); + + await _repo.DeleteAsync(po); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/ReceivePaymentsController.cs b/src/Presentation/QBD.API/Controllers/ReceivePaymentsController.cs new file mode 100644 index 0000000..10781c0 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/ReceivePaymentsController.cs @@ -0,0 +1,89 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ReceivePaymentsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly ITransactionPostingService _posting; + + public ReceivePaymentsController( + IRepository repo, + IUnitOfWork uow, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? customerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(r => r.Customer).AsQueryable(); + if (customerId.HasValue) query = query.Where(r => r.CustomerId == customerId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(r => r.PaymentDate) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var payment = await _repo.Query() + .Include(r => r.Customer) + .Include(r => r.Applications).ThenInclude(a => a.Invoice) + .Include(r => r.PaymentMethod) + .Include(r => r.DepositToAccount) + .FirstOrDefaultAsync(r => r.Id == id); + if (payment == null) return NotFound(); + return Ok(payment); + } + + [HttpPost] + public async Task Create([FromBody] ReceivePayment payment) + { + payment.Status = DocStatus.Draft; + var created = await _repo.AddAsync(payment); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var payment = await _repo.GetByIdAsync(id); + if (payment == null) return NotFound(); + if (payment.Status != DocStatus.Draft) return BadRequest("Only draft payments can be posted."); + + await _posting.PostTransactionAsync(TransactionType.ReceivePayment, id); + payment.Status = DocStatus.Posted; + await _repo.UpdateAsync(payment); + await _uow.SaveChangesAsync(); + return Ok(payment); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var payment = await _repo.GetByIdAsync(id); + if (payment == null) return NotFound(); + if (payment.Status == DocStatus.Posted) return BadRequest("Cannot delete posted payments."); + + await _repo.DeleteAsync(payment); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/SalesReceiptsController.cs b/src/Presentation/QBD.API/Controllers/SalesReceiptsController.cs new file mode 100644 index 0000000..225e8bd --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/SalesReceiptsController.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Customers; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SalesReceiptsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public SalesReceiptsController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(s => s.Customer); + var total = await query.CountAsync(); + var items = await query.OrderByDescending(s => s.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var receipt = await _repo.Query() + .Include(s => s.Customer) + .Include(s => s.Lines).ThenInclude(l => l.Item) + .Include(s => s.PaymentMethod) + .FirstOrDefaultAsync(s => s.Id == id); + if (receipt == null) return NotFound(); + return Ok(receipt); + } + + [HttpPost] + public async Task Create([FromBody] SalesReceipt receipt) + { + receipt.SalesReceiptNumber = await _numberSeq.GetNextNumberAsync("SalesReceipt"); + receipt.Status = DocStatus.Draft; + receipt.Subtotal = receipt.Lines.Sum(l => l.Amount); + receipt.Total = receipt.Subtotal + receipt.Tax; + + var created = await _repo.AddAsync(receipt); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var receipt = await _repo.GetByIdAsync(id); + if (receipt == null) return NotFound(); + if (receipt.Status != DocStatus.Draft) return BadRequest("Only draft sales receipts can be posted."); + + await _posting.PostTransactionAsync(TransactionType.SalesReceipt, id); + receipt.Status = DocStatus.Posted; + await _repo.UpdateAsync(receipt); + await _uow.SaveChangesAsync(); + return Ok(receipt); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var receipt = await _repo.GetByIdAsync(id); + if (receipt == null) return NotFound(); + if (receipt.Status == DocStatus.Posted) return BadRequest("Cannot delete posted sales receipts."); + + await _repo.DeleteAsync(receipt); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/TransfersController.cs b/src/Presentation/QBD.API/Controllers/TransfersController.cs new file mode 100644 index 0000000..b045a73 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/TransfersController.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Banking; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class TransfersController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly ITransactionPostingService _posting; + + public TransfersController( + IRepository repo, + IUnitOfWork uow, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query() + .Include(t => t.FromAccount) + .Include(t => t.ToAccount); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(t => t.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var transfer = await _repo.Query() + .Include(t => t.FromAccount) + .Include(t => t.ToAccount) + .FirstOrDefaultAsync(t => t.Id == id); + if (transfer == null) return NotFound(); + return Ok(transfer); + } + + [HttpPost] + public async Task Create([FromBody] Transfer transfer) + { + transfer.Status = DocStatus.Draft; + var created = await _repo.AddAsync(transfer); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var transfer = await _repo.GetByIdAsync(id); + if (transfer == null) return NotFound(); + if (transfer.Status != DocStatus.Draft) return BadRequest("Only draft transfers can be posted."); + + await _posting.PostTransactionAsync(TransactionType.Transfer, id); + transfer.Status = DocStatus.Posted; + await _repo.UpdateAsync(transfer); + await _uow.SaveChangesAsync(); + return Ok(transfer); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var transfer = await _repo.GetByIdAsync(id); + if (transfer == null) return NotFound(); + if (transfer.Status == DocStatus.Posted) return BadRequest("Cannot delete posted transfers."); + + await _repo.DeleteAsync(transfer); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/VendorCreditsController.cs b/src/Presentation/QBD.API/Controllers/VendorCreditsController.cs new file mode 100644 index 0000000..0065c51 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/VendorCreditsController.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Vendors; +using QBD.Domain.Enums; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class VendorCreditsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + private readonly INumberSequenceService _numberSeq; + private readonly ITransactionPostingService _posting; + + public VendorCreditsController( + IRepository repo, + IUnitOfWork uow, + INumberSequenceService numberSeq, + ITransactionPostingService posting) + { + _repo = repo; + _uow = uow; + _numberSeq = numberSeq; + _posting = posting; + } + + [HttpGet] + public async Task GetAll([FromQuery] int? vendorId, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var query = _repo.Query().Include(v => v.Vendor).AsQueryable(); + if (vendorId.HasValue) query = query.Where(v => v.VendorId == vendorId.Value); + + var total = await query.CountAsync(); + var items = await query.OrderByDescending(v => v.Date) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var credit = await _repo.Query() + .Include(v => v.Vendor) + .Include(v => v.Lines).ThenInclude(l => l.Account) + .Include(v => v.Lines).ThenInclude(l => l.Item) + .FirstOrDefaultAsync(v => v.Id == id); + if (credit == null) return NotFound(); + return Ok(credit); + } + + [HttpPost] + public async Task Create([FromBody] VendorCredit credit) + { + credit.RefNo = await _numberSeq.GetNextNumberAsync("VendorCredit"); + credit.Status = DocStatus.Draft; + credit.Total = credit.Lines.Sum(l => l.Amount); + credit.BalanceRemaining = credit.Total; + + var created = await _repo.AddAsync(credit); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPost("{id}/post")] + public async Task Post(int id) + { + var credit = await _repo.GetByIdAsync(id); + if (credit == null) return NotFound(); + if (credit.Status != DocStatus.Draft) return BadRequest("Only draft vendor credits can be posted."); + + await _posting.PostTransactionAsync(TransactionType.VendorCredit, id); + credit.Status = DocStatus.Posted; + await _repo.UpdateAsync(credit); + await _uow.SaveChangesAsync(); + return Ok(credit); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var credit = await _repo.GetByIdAsync(id); + if (credit == null) return NotFound(); + if (credit.Status == DocStatus.Posted) return BadRequest("Cannot delete posted vendor credits."); + + await _repo.DeleteAsync(credit); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Controllers/VendorsController.cs b/src/Presentation/QBD.API/Controllers/VendorsController.cs new file mode 100644 index 0000000..fa0db77 --- /dev/null +++ b/src/Presentation/QBD.API/Controllers/VendorsController.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Domain.Entities.Vendors; + +namespace QBD.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class VendorsController : ControllerBase +{ + private readonly IRepository _repo; + private readonly IUnitOfWork _uow; + + public VendorsController(IRepository repo, IUnitOfWork uow) + { + _repo = repo; + _uow = uow; + } + + [HttpGet] + public async Task GetAll([FromQuery] bool? activeOnly, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var (items, total) = await _repo.GetPagedAsync(page, pageSize, + filter: activeOnly == true ? v => v.IsActive : null, + orderBy: v => v.VendorName); + return Ok(new { items, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var vendor = await _repo.Query() + .Include(v => v.Bills) + .Include(v => v.Terms) + .FirstOrDefaultAsync(v => v.Id == id); + if (vendor == null) return NotFound(); + return Ok(vendor); + } + + [HttpPost] + public async Task Create([FromBody] Vendor vendor) + { + vendor.IsActive = true; + var created = await _repo.AddAsync(vendor); + await _uow.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Vendor vendor) + { + var existing = await _repo.GetByIdAsync(id); + if (existing == null) return NotFound(); + + existing.VendorName = vendor.VendorName; + existing.Company = vendor.Company; + existing.Address = vendor.Address; + existing.Phone = vendor.Phone; + existing.Email = vendor.Email; + existing.TermsId = vendor.TermsId; + existing.CreditLimit = vendor.CreditLimit; + existing.TaxId = vendor.TaxId; + existing.Is1099 = vendor.Is1099; + existing.IsActive = vendor.IsActive; + + await _repo.UpdateAsync(existing); + await _uow.SaveChangesAsync(); + return Ok(existing); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var vendor = await _repo.GetByIdAsync(id); + if (vendor == null) return NotFound(); + + await _repo.DeleteAsync(vendor); + await _uow.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/Presentation/QBD.API/Program.cs b/src/Presentation/QBD.API/Program.cs new file mode 100644 index 0000000..0c83430 --- /dev/null +++ b/src/Presentation/QBD.API/Program.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using Microsoft.EntityFrameworkCore; +using QBD.Application.Interfaces; +using QBD.Infrastructure.Data; +using QBD.Infrastructure.Repositories; +using QBD.Infrastructure.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Database - SQLite for cross-platform support +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") + ?? "Data Source=QuickBooksDesktop.db")); + +// Repositories +builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); +builder.Services.AddScoped(); + +// Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Seeder +builder.Services.AddScoped(); + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; + }); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "QuickBooks Desktop API", Version = "v1" }); +}); + +var app = builder.Build(); + +// Ensure database is created and seeded +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(); + + var seeder = scope.ServiceProvider.GetRequiredService(); + await seeder.SeedAsync(); +} + +app.UseSwagger(); +app.UseSwaggerUI(); + +app.MapControllers(); + +app.Run(); diff --git a/src/Presentation/QBD.API/Properties/launchSettings.json b/src/Presentation/QBD.API/Properties/launchSettings.json new file mode 100644 index 0000000..ca55329 --- /dev/null +++ b/src/Presentation/QBD.API/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5081", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7196;http://localhost:5081", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Presentation/QBD.API/QBD.API.csproj b/src/Presentation/QBD.API/QBD.API.csproj new file mode 100644 index 0000000..9c59a09 --- /dev/null +++ b/src/Presentation/QBD.API/QBD.API.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + QBD.API + + + + + + + + + + + + + + diff --git a/src/Presentation/QBD.API/QuickBooksDesktop.db b/src/Presentation/QBD.API/QuickBooksDesktop.db new file mode 100644 index 0000000..724fe81 Binary files /dev/null and b/src/Presentation/QBD.API/QuickBooksDesktop.db differ diff --git a/src/Presentation/QBD.API/appsettings.Development.json b/src/Presentation/QBD.API/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/src/Presentation/QBD.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Presentation/QBD.API/appsettings.json b/src/Presentation/QBD.API/appsettings.json new file mode 100644 index 0000000..f203ae4 --- /dev/null +++ b/src/Presentation/QBD.API/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Data Source=QuickBooksDesktop.db" + } +} diff --git a/src/Presentation/QBD.WPF/App.xaml b/src/Presentation/QBD.WPF/App.xaml index 89033b4..ef6edb9 100644 --- a/src/Presentation/QBD.WPF/App.xaml +++ b/src/Presentation/QBD.WPF/App.xaml @@ -1,3 +1,6 @@ + + + diff --git a/src/Presentation/QBD.WPF/App.xaml.cs b/src/Presentation/QBD.WPF/App.xaml.cs index 92b5722..905cb29 100644 --- a/src/Presentation/QBD.WPF/App.xaml.cs +++ b/src/Presentation/QBD.WPF/App.xaml.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Windows; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -33,6 +36,8 @@ public App() services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddTransient(); + services.AddSingleton(); // Navigation services.AddSingleton(); diff --git a/src/Presentation/QBD.WPF/AssemblyInfo.cs b/src/Presentation/QBD.WPF/AssemblyInfo.cs index cc29e7f..0eb371b 100644 --- a/src/Presentation/QBD.WPF/AssemblyInfo.cs +++ b/src/Presentation/QBD.WPF/AssemblyInfo.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Windows; [assembly:ThemeInfo( diff --git a/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs b/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs index 6679cc5..5a8f71d 100644 --- a/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs +++ b/src/Presentation/QBD.WPF/Controls/ViewModelTemplateSelector.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System.Windows; using System.Windows.Controls; using QBD.Application.ViewModels; diff --git a/src/Presentation/QBD.WPF/MainWindow.xaml b/src/Presentation/QBD.WPF/MainWindow.xaml index 38ac16e..f64035f 100644 --- a/src/Presentation/QBD.WPF/MainWindow.xaml +++ b/src/Presentation/QBD.WPF/MainWindow.xaml @@ -1,3 +1,6 @@ + + + + + + + + + + + + + + - + @@ -118,8 +132,11 @@ + + + diff --git a/src/Presentation/QBD.WPF/MainWindow.xaml.cs b/src/Presentation/QBD.WPF/MainWindow.xaml.cs index 03fe618..1ae1e09 100644 --- a/src/Presentation/QBD.WPF/MainWindow.xaml.cs +++ b/src/Presentation/QBD.WPF/MainWindow.xaml.cs @@ -1,7 +1,12 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using System; using System.IO; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; using QBD.Application.Interfaces; using QBD.Application.ViewModels; @@ -13,18 +18,32 @@ public partial class MainWindow : Window private bool _isDarkMode = false; private string _themeConfigFile = string.Empty; + public ICommand HomePageCommand { get; } + public ICommand FocusSearchCommand { get; } + public ICommand CloseCurrentTabCommand { get; } + public ICommand SaveCommand { get; } + public ICommand NewCommand { get; } + public ICommand ShowKeyboardShortcutsCommand { get; } + public MainWindow(INavigationService navigationService, HomePageViewModel homePageViewModel) { - InitializeComponent(); _navigationService = navigationService; + HomePageCommand = new ActionCommand(() => _navigationService.OpenHomePage()); + FocusSearchCommand = new ActionCommand(() => FocusSearch()); + CloseCurrentTabCommand = new ActionCommand(() => CloseCurrentTab()); + SaveCommand = new ActionCommand(() => ExecuteGlobalSave()); + NewCommand = new ActionCommand(() => ExecuteGlobalNew()); + ShowKeyboardShortcutsCommand = new ActionCommand(() => ShowKeyboardShortcuts()); + + InitializeComponent(); + + this.DataContext = this; + try { var appDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "QBD", "QBD.WPF"); - if (!Directory.Exists(appDataDirectory)) - { - Directory.CreateDirectory(appDataDirectory); - } + if (!Directory.Exists(appDataDirectory)) Directory.CreateDirectory(appDataDirectory); _themeConfigFile = Path.Combine(appDataDirectory, "theme.cfg"); @@ -51,8 +70,7 @@ public void OpenTab(ViewModelBase viewModel) { foreach (var item in WorkspaceTabs.Items) { - if (item is ViewModelBase existing && existing.GetType() == viewModel.GetType() - && existing.Title == viewModel.Title) + if (item is ViewModelBase existing && existing.GetType() == viewModel.GetType() && existing.Title == viewModel.Title) { WorkspaceTabs.SelectedItem = item; return; @@ -62,15 +80,8 @@ public void OpenTab(ViewModelBase viewModel) WorkspaceTabs.SelectedItem = viewModel; } - public void CloseTab(object viewModel) - { - WorkspaceTabs.Items.Remove(viewModel); - } - - public void CloseAllTabs() - { - WorkspaceTabs.Items.Clear(); - } + public void CloseTab(object viewModel) => WorkspaceTabs.Items.Remove(viewModel); + public void CloseAllTabs() => WorkspaceTabs.Items.Clear(); private void Exit_Click(object sender, RoutedEventArgs e) => System.Windows.Application.Current.Shutdown(); private void HomePage_Click(object sender, RoutedEventArgs e) => _navigationService.OpenHomePage(); @@ -116,36 +127,243 @@ public void CloseAllTabs() private void CloseTab_Click(object sender, RoutedEventArgs e) { - if (sender is Button btn && btn.DataContext is ViewModelBase vm) - { - CloseTab(vm); - } + if (sender is Button btn && btn.DataContext is ViewModelBase vm) CloseTab(vm); } - private void CloseAllTabs_Click(object sender, RoutedEventArgs e) => CloseAllTabs(); + private void KeyboardShortcuts_Click(object sender, RoutedEventArgs e) => ShowKeyboardShortcuts(); + + private void ShowKeyboardShortcuts() + { + var shortcuts = new Window + { + Title = "Keyboard Shortcuts", + Width = 450, + Height = 480, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Owner = this, + ResizeMode = ResizeMode.NoResize, + Background = (System.Windows.Media.Brush)FindResource("ThemeWindowBackground") + }; + + var grid = new Grid { Margin = new Thickness(20) }; + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + var header = new TextBlock + { + Text = "Keyboard Shortcuts", + FontSize = 18, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 0, 0, 12), + Foreground = (System.Windows.Media.Brush)FindResource("ThemeForeground") + }; + Grid.SetRow(header, 0); + + var items = new[] + { + ("F1", "Show this help dialog"), + ("F5", "Go to Home Page"), + ("Ctrl+N", "New transaction (context-aware)"), + ("Ctrl+S", "Save current form"), + ("Ctrl+F", "Focus search box"), + ("Ctrl+A", "Chart of Accounts"), + ("Esc", "Close current tab"), + ("Tab", "Move to next field"), + ("Shift+Tab", "Move to previous field"), + ("Alt+S", "Save (in forms)"), + ("Alt+P", "Save & Post (in forms)"), + ("Alt+L", "Clear (in forms)"), + ("Alt+V", "Void (in forms)"), + ("Alt+R", "Print (in forms)"), + ("Alt+N", "New (in lists)"), + ("Alt+E", "Edit (in lists)"), + ("Alt+D", "Delete (in lists)"), + }; + + var listView = new ListView + { + BorderThickness = new Thickness(1), + BorderBrush = (System.Windows.Media.Brush)FindResource("ThemeBorderBrush"), + Background = (System.Windows.Media.Brush)FindResource("ThemeControlBackground") + }; + + var gridView = new GridView(); + gridView.Columns.Add(new GridViewColumn + { + Header = "Shortcut", + Width = 120, + DisplayMemberBinding = new System.Windows.Data.Binding("Item1") + }); + gridView.Columns.Add(new GridViewColumn + { + Header = "Action", + Width = 280, + DisplayMemberBinding = new System.Windows.Data.Binding("Item2") + }); + listView.View = gridView; + + foreach (var item in items) + listView.Items.Add(item); + + Grid.SetRow(listView, 1); + + var closeBtn = new Button + { + Content = "Close", + Padding = new Thickness(20, 6, 20, 6), + HorizontalAlignment = HorizontalAlignment.Right, + Margin = new Thickness(0, 12, 0, 0) + }; + closeBtn.Click += (_, _) => shortcuts.Close(); + Grid.SetRow(closeBtn, 2); + + grid.Children.Add(header); + grid.Children.Add(listView); + grid.Children.Add(closeBtn); + shortcuts.Content = grid; + shortcuts.ShowDialog(); + } + private void About_Click(object sender, RoutedEventArgs e) { - MessageBox.Show("QuickBooks Desktop Enterprise Clone\nVersion 1.0\n\nA full-featured accounting application.", - "About", MessageBoxButton.OK, MessageBoxImage.Information); + MessageBox.Show("QuickBooks Desktop Enterprise Clone\nVersion 1.0\n\nA full-featured accounting application.", "About", MessageBoxButton.OK, MessageBoxImage.Information); } private void ToggleDarkMode_Click(object sender, RoutedEventArgs e) { _isDarkMode = !_isDarkMode; - var app = (App)System.Windows.Application.Current; - app.ChangeTheme(_isDarkMode); + ((App)System.Windows.Application.Current).ChangeTheme(_isDarkMode); + try { if (!string.IsNullOrEmpty(_themeConfigFile)) File.WriteAllText(_themeConfigFile, _isDarkMode.ToString()); } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error saving theme: {ex.Message}"); } + } - try + private void CloseCurrentTab() + { + if (WorkspaceTabs.SelectedItem != null) CloseTab(WorkspaceTabs.SelectedItem); + } + + private void FocusSearch() + { + var contentPresenter = FindVisualChild(WorkspaceTabs); + var searchRoot = contentPresenter ?? (DependencyObject)WorkspaceTabs; + var searchBox = FindSearchTextBox(searchRoot); + + if (searchBox != null) + { + searchBox.Focus(); + Keyboard.Focus(searchBox); + StatusText.Text = "Search box focused (Ctrl+F)."; + } + else + { + StatusText.Text = "No search box found in this view."; + } + } + + private void ExecuteGlobalNew() + { + var selectedItem = WorkspaceTabs.SelectedItem; + if (selectedItem == null) return; + + StatusText.Text = "Opening new entry form..."; + + var itemType = selectedItem.GetType(); + string[] commandNames = { "NewEntityCommand", "NewItemCommand", "NewCommand" }; + + foreach (var name in commandNames) { - if (!string.IsNullOrEmpty(_themeConfigFile)) + var prop = itemType.GetProperty(name); + if (prop != null && prop.GetValue(selectedItem) is ICommand command && command.CanExecute(null)) { - File.WriteAllText(_themeConfigFile, _isDarkMode.ToString()); + command.Execute(null); + (WorkspaceTabs.SelectedContent as FrameworkElement)?.Focus(); + return; } } - catch (Exception ex) + StatusText.Text = "New entry not available here."; + } + + private async void ExecuteGlobalSave() + { + var selectedItem = WorkspaceTabs.SelectedItem; + if (selectedItem == null) return; + + var prop = selectedItem.GetType().GetProperty("SaveCommand"); + if (prop != null && prop.GetValue(selectedItem) is ICommand command && command.CanExecute(null)) { - System.Diagnostics.Debug.WriteLine($"Error saving theme: {ex.Message}"); + StatusText.Text = "Saving..."; + try + { + var asyncMethod = command.GetType().GetMethod("ExecuteAsync"); + if (asyncMethod != null && asyncMethod.Invoke(command, new object?[] { null }) is Task task) + { + await task; + } + else + { + command.Execute(null); + } + StatusText.Text = "Transaction saved successfully (Ctrl+S)."; + } + catch (Exception ex) + { + StatusText.Text = "Save failed."; + System.Diagnostics.Debug.WriteLine($"Save error: {ex.Message}"); + } + + (WorkspaceTabs.SelectedContent as FrameworkElement)?.Focus(); + } + else + { + StatusText.Text = "Save not supported in this view."; + } + } + + private TextBox? FindSearchTextBox(DependencyObject? obj) + { + if (obj == null) return null; + + for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(obj); i++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(obj, i); + + if (child is TextBox tb) + { + var binding = tb.GetBindingExpression(TextBox.TextProperty); + var path = binding?.ParentBinding?.Path?.Path; + if (path is "FilterText" or "SearchText") + return tb; + } + + var result = FindSearchTextBox(child); + if (result != null) return result; } + return null; } + + private T? FindVisualChild(DependencyObject? obj) where T : DependencyObject + { + if (obj == null) return null; + + for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(obj); i++) + { + var child = System.Windows.Media.VisualTreeHelper.GetChild(obj, i); + if (child is T match) return match; + + var result = FindVisualChild(child); + if (result != null) return result; + } + return null; + } +} + +public class ActionCommand : ICommand +{ + private readonly Action _execute; + public ActionCommand(Action execute) => _execute = execute; + public bool CanExecute(object? parameter) => true; + public void Execute(object? parameter) => _execute(); + public event EventHandler? CanExecuteChanged { add { } remove { } } } \ No newline at end of file diff --git a/src/Presentation/QBD.WPF/QBD.WPF.csproj b/src/Presentation/QBD.WPF/QBD.WPF.csproj index d392753..7ed91dc 100644 --- a/src/Presentation/QBD.WPF/QBD.WPF.csproj +++ b/src/Presentation/QBD.WPF/QBD.WPF.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Presentation/QBD.WPF/Services/NavigationService.cs b/src/Presentation/QBD.WPF/Services/NavigationService.cs index 4441a55..5ea665d 100644 --- a/src/Presentation/QBD.WPF/Services/NavigationService.cs +++ b/src/Presentation/QBD.WPF/Services/NavigationService.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + using QBD.Application.Interfaces; using QBD.Application.ViewModels; diff --git a/src/Presentation/QBD.WPF/Services/WpfFileDialogService.cs b/src/Presentation/QBD.WPF/Services/WpfFileDialogService.cs new file mode 100644 index 0000000..3f9b902 --- /dev/null +++ b/src/Presentation/QBD.WPF/Services/WpfFileDialogService.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2026, Ravindu Gajanayaka +// Licensed under GPLv3. See LICENSE + +using QBD.Application.Interfaces; + +namespace QBD.WPF.Services; + +public class WpfFileDialogService : IFileDialogService +{ + public string? ShowSaveFileDialog(string fileName, string defaultExt, string filter) + { + var dialog = new Microsoft.Win32.SaveFileDialog + { + FileName = fileName, + DefaultExt = defaultExt, + Filter = filter + }; + + return dialog.ShowDialog() == true ? dialog.FileName : null; + } +} diff --git a/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml b/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml index 696d3c8..e7d4dc2 100644 --- a/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml +++ b/src/Presentation/QBD.WPF/Themes/ClassicTheme.xaml @@ -1,3 +1,6 @@ + + + @@ -22,7 +25,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}"> - + @@ -55,7 +58,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" Padding="{TemplateBinding Padding}"> - + diff --git a/src/Presentation/QBD.WPF/Themes/DarkColors.xaml b/src/Presentation/QBD.WPF/Themes/DarkColors.xaml index 46ebe98..acbe065 100644 --- a/src/Presentation/QBD.WPF/Themes/DarkColors.xaml +++ b/src/Presentation/QBD.WPF/Themes/DarkColors.xaml @@ -1,3 +1,6 @@ + + + diff --git a/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml b/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml index a2a5563..333eb25 100644 --- a/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml +++ b/src/Presentation/QBD.WPF/Themes/DataTemplates.xaml @@ -1,3 +1,6 @@ + + + @@ -107,19 +110,19 @@ -